1use reqwest::Url;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::collections::HashMap;
5use std::convert::TryFrom;
6use std::fmt;
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::{Arc, Weak};
10use std::time::SystemTime;
11use url::form_urlencoded::byte_serialize;
12
13use crate::app::FirebaseApp;
14use crate::auth::api::Auth;
15use crate::auth::error::{AuthError, AuthResult};
16use crate::auth::model::{MfaEnrollmentInfo, User, UserCredential};
17use crate::auth::phone::PhoneAuthCredential;
18use crate::auth::PHONE_PROVIDER_ID;
19use crate::util::base64::base64_decode_bytes;
20use crate::util::PartialObserver;
21
22#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
23pub struct IdTokenResult {
24 pub token: String,
25 pub auth_time: Option<String>,
26 pub issued_at_time: Option<String>,
27 pub expiration_time: Option<String>,
28 pub sign_in_provider: Option<String>,
29 pub sign_in_second_factor: Option<String>,
30 pub claims: Value,
31}
32
33#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
34pub struct UserMetadata {
35 pub creation_time: Option<String>,
36 pub last_sign_in_time: Option<String>,
37}
38
39#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
40pub struct ActionCodeSettings {
41 pub url: String,
42 pub handle_code_in_app: bool,
43 pub i_os: Option<IosSettings>,
44 pub android: Option<AndroidSettings>,
45 pub dynamic_link_domain: Option<String>,
46 pub link_domain: Option<String>,
47}
48
49#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
50pub struct IosSettings {
51 pub bundle_id: String,
52}
53
54#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
55pub struct AndroidSettings {
56 pub package_name: String,
57 pub install_app: Option<bool>,
58 pub minimum_version: Option<String>,
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
62pub enum ActionCodeOperation {
63 PasswordReset,
64 RecoverEmail,
65 EmailSignIn,
66 RevertSecondFactorAddition,
67 VerifyAndChangeEmail,
68 #[default]
69 VerifyEmail,
70}
71
72impl ActionCodeOperation {
73 pub fn as_request_type(&self) -> &'static str {
75 match self {
76 ActionCodeOperation::PasswordReset => "PASSWORD_RESET",
77 ActionCodeOperation::RecoverEmail => "RECOVER_EMAIL",
78 ActionCodeOperation::EmailSignIn => "EMAIL_SIGNIN",
79 ActionCodeOperation::RevertSecondFactorAddition => "REVERT_SECOND_FACTOR_ADDITION",
80 ActionCodeOperation::VerifyAndChangeEmail => "VERIFY_AND_CHANGE_EMAIL",
81 ActionCodeOperation::VerifyEmail => "VERIFY_EMAIL",
82 }
83 }
84
85 pub fn from_request_type(value: &str) -> Option<Self> {
87 match value {
88 "PASSWORD_RESET" => Some(ActionCodeOperation::PasswordReset),
89 "RECOVER_EMAIL" => Some(ActionCodeOperation::RecoverEmail),
90 "EMAIL_SIGNIN" => Some(ActionCodeOperation::EmailSignIn),
91 "REVERT_SECOND_FACTOR_ADDITION" => {
92 Some(ActionCodeOperation::RevertSecondFactorAddition)
93 }
94 "VERIFY_AND_CHANGE_EMAIL" => Some(ActionCodeOperation::VerifyAndChangeEmail),
95 "VERIFY_EMAIL" => Some(ActionCodeOperation::VerifyEmail),
96 _ => None,
97 }
98 }
99
100 pub fn from_mode(value: &str) -> Option<Self> {
102 match value {
103 "recoverEmail" => Some(ActionCodeOperation::RecoverEmail),
104 "resetPassword" => Some(ActionCodeOperation::PasswordReset),
105 "signIn" => Some(ActionCodeOperation::EmailSignIn),
106 "verifyEmail" => Some(ActionCodeOperation::VerifyEmail),
107 "verifyAndChangeEmail" => Some(ActionCodeOperation::VerifyAndChangeEmail),
108 "revertSecondFactorAddition" => Some(ActionCodeOperation::RevertSecondFactorAddition),
109 _ => None,
110 }
111 }
112}
113
114#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
115pub struct ActionCodeInfoData {
116 pub email: Option<String>,
117 pub previous_email: Option<String>,
118 pub multi_factor_info: Option<MultiFactorInfo>,
119 pub from_email: Option<String>,
120}
121
122#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
123pub struct ActionCodeInfo {
124 pub data: ActionCodeInfoData,
125 pub operation: ActionCodeOperation,
126}
127
128#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
129pub struct ActionCodeUrl {
130 pub api_key: String,
131 pub code: String,
132 pub continue_url: Option<String>,
133 pub language_code: Option<String>,
134 pub tenant_id: Option<String>,
135 pub operation: ActionCodeOperation,
136}
137
138impl ActionCodeUrl {
139 pub fn parse(link: &str) -> Option<Self> {
141 let resolved_link = resolve_action_link(link)?;
142 let parsed = Url::parse(&resolved_link).ok()?;
143 let query: HashMap<_, _> = parsed.query_pairs().into_owned().collect();
144 let api_key = query.get("apiKey")?.clone();
145 let code = query.get("oobCode")?.clone();
146 let operation = query
147 .get("mode")
148 .and_then(|mode| ActionCodeOperation::from_mode(mode))?;
149 let language_code = query
150 .get("lang")
151 .cloned()
152 .or_else(|| query.get("languageCode").cloned());
153 Some(Self {
154 api_key,
155 code,
156 continue_url: query.get("continueUrl").cloned(),
157 language_code,
158 tenant_id: query.get("tenantId").cloned(),
159 operation,
160 })
161 }
162}
163
164fn resolve_action_link(link: &str) -> Option<String> {
165 fn helper(original: &str, depth: usize) -> Option<String> {
166 if depth > 4 {
167 return Some(original.to_string());
168 }
169 let parsed = Url::parse(original).ok()?;
170 let query: HashMap<_, _> = parsed.query_pairs().into_owned().collect();
171
172 if let Some(link_value) = query.get("link") {
173 if let Some(resolved) = helper(link_value, depth + 1) {
174 return Some(resolved);
175 }
176 return Some(link_value.clone());
177 }
178
179 if let Some(deep_link) = query.get("deep_link_id") {
180 if let Some(resolved) = helper(deep_link, depth + 1) {
181 return Some(resolved);
182 }
183 return Some(deep_link.clone());
184 }
185
186 Some(original.to_string())
187 }
188
189 helper(link, 0)
190}
191
192#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
193pub struct AdditionalUserInfo {
194 pub is_new_user: bool,
195 pub provider_id: Option<String>,
196 pub profile: Option<Value>,
197 pub username: Option<String>,
198}
199
200#[cfg(target_arch = "wasm32")]
201type ConfirmationFuture = Pin<Box<dyn Future<Output = AuthResult<UserCredential>> + 'static>>;
202
203#[cfg(not(target_arch = "wasm32"))]
204type ConfirmationFuture =
205 Pin<Box<dyn Future<Output = AuthResult<UserCredential>> + Send + 'static>>;
206
207#[cfg(target_arch = "wasm32")]
208type ConfirmationHandler = Arc<dyn Fn(&str) -> ConfirmationFuture + 'static>;
209
210#[cfg(not(target_arch = "wasm32"))]
211type ConfirmationHandler = Arc<dyn Fn(&str) -> ConfirmationFuture + Send + Sync + 'static>;
212
213pub struct ConfirmationResult {
214 verification_id: String,
215 confirm_handler: ConfirmationHandler,
216}
217
218impl ConfirmationResult {
219 #[cfg(target_arch = "wasm32")]
221 pub fn new<F, Fut>(verification_id: String, confirm_handler: F) -> Self
222 where
223 F: Fn(&str) -> Fut + 'static,
224 Fut: Future<Output = AuthResult<UserCredential>> + 'static,
225 {
226 let handler = move |code: &str| -> ConfirmationFuture {
227 let fut = confirm_handler(code);
228 Box::pin(fut)
229 };
230 Self {
231 verification_id,
232 confirm_handler: Arc::new(handler),
233 }
234 }
235
236 #[cfg(not(target_arch = "wasm32"))]
238 pub fn new<F, Fut>(verification_id: String, confirm_handler: F) -> Self
239 where
240 F: Fn(&str) -> Fut + Send + Sync + 'static,
241 Fut: Future<Output = AuthResult<UserCredential>> + Send + 'static,
242 {
243 let handler = move |code: &str| -> ConfirmationFuture {
244 let fut = confirm_handler(code);
245 Box::pin(fut)
246 };
247 Self {
248 verification_id,
249 confirm_handler: Arc::new(handler),
250 }
251 }
252
253 pub async fn confirm(&self, verification_code: &str) -> AuthResult<UserCredential> {
255 (self.confirm_handler)(verification_code).await
256 }
257
258 pub fn verification_id(&self) -> &str {
260 &self.verification_id
261 }
262}
263
264impl Clone for ConfirmationResult {
265 fn clone(&self) -> Self {
266 Self {
267 verification_id: self.verification_id.clone(),
268 confirm_handler: self.confirm_handler.clone(),
269 }
270 }
271}
272
273#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
274pub struct AuthSettings {
275 pub app_verification_disabled_for_testing: bool,
276}
277
278pub trait ApplicationVerifier: Send + Sync {
279 fn verify(&self) -> AuthResult<String>;
280 fn verifier_type(&self) -> &str;
281}
282
283#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
284pub struct MultiFactorInfo {
285 pub uid: String,
286 pub display_name: Option<String>,
287 pub enrollment_time: Option<String>,
288 pub factor_id: String,
289}
290
291impl MultiFactorInfo {
292 pub(crate) fn from_enrollment(enrollment: &MfaEnrollmentInfo) -> Option<Self> {
293 let uid = enrollment.mfa_enrollment_id.clone()?;
294 let factor_id = enrollment
295 .factor_id
296 .clone()
297 .or_else(|| {
298 enrollment
299 .phone_info
300 .as_ref()
301 .map(|_| PHONE_PROVIDER_ID.to_string())
302 })
303 .unwrap_or_else(|| "unknown".to_string());
304
305 let display_name = if factor_id == WEBAUTHN_FACTOR_ID {
306 enrollment.display_name.clone().or_else(|| {
307 enrollment
308 .webauthn_info
309 .as_ref()
310 .and_then(|info| info.get("displayName"))
311 .and_then(|value| value.as_str())
312 .map(|value| value.to_string())
313 })
314 } else {
315 enrollment.display_name.clone()
316 };
317
318 Some(Self {
319 uid,
320 display_name,
321 enrollment_time: enrollment
322 .enrolled_at
323 .as_ref()
324 .map(|value| value.to_string()),
325 factor_id,
326 })
327 }
328}
329
330#[derive(Clone, Copy, Debug, PartialEq, Eq)]
332pub enum MultiFactorSessionType {
333 Enrollment,
335 SignIn,
337}
338
339#[derive(Clone, Copy, Debug, PartialEq, Eq)]
341pub enum MultiFactorOperation {
342 SignIn,
344 Reauthenticate,
346 Link,
348}
349
350#[derive(Clone, Debug)]
351pub struct MultiFactorSession {
352 kind: MultiFactorSessionType,
353 credential: String,
354}
355
356impl MultiFactorSession {
357 pub(crate) fn enrollment(id_token: String) -> Self {
358 Self {
359 kind: MultiFactorSessionType::Enrollment,
360 credential: id_token,
361 }
362 }
363
364 pub(crate) fn sign_in(pending_credential: String) -> Self {
365 Self {
366 kind: MultiFactorSessionType::SignIn,
367 credential: pending_credential,
368 }
369 }
370
371 pub fn credential(&self) -> &str {
376 &self.credential
377 }
378
379 pub fn session_type(&self) -> MultiFactorSessionType {
381 self.kind
382 }
383
384 pub fn id_token(&self) -> Option<&str> {
386 match self.kind {
387 MultiFactorSessionType::Enrollment => Some(&self.credential),
388 MultiFactorSessionType::SignIn => None,
389 }
390 }
391
392 pub fn pending_credential(&self) -> Option<&str> {
394 match self.kind {
395 MultiFactorSessionType::SignIn => Some(&self.credential),
396 MultiFactorSessionType::Enrollment => None,
397 }
398 }
399}
400
401#[derive(Clone, Debug)]
402pub(crate) struct MultiFactorSignInContext {
403 pub local_id: Option<String>,
404 pub email: Option<String>,
405 pub phone_number: Option<String>,
406 pub provider_id: Option<String>,
407 pub is_new_user: Option<bool>,
408 pub anonymous: bool,
409}
410
411impl Default for MultiFactorSignInContext {
412 fn default() -> Self {
413 Self {
414 local_id: None,
415 email: None,
416 phone_number: None,
417 provider_id: None,
418 is_new_user: None,
419 anonymous: false,
420 }
421 }
422}
423
424impl MultiFactorSignInContext {
425 pub(crate) fn operation_label(&self, operation: MultiFactorOperation) -> &'static str {
426 match operation {
427 MultiFactorOperation::SignIn => {
428 if self.is_new_user.unwrap_or(false) {
429 "signUp"
430 } else {
431 "signIn"
432 }
433 }
434 MultiFactorOperation::Reauthenticate => "reauthenticate",
435 MultiFactorOperation::Link => "link",
436 }
437 }
438}
439
440#[derive(Clone, Debug)]
441pub struct PhoneMultiFactorAssertion {
442 credential: PhoneAuthCredential,
443}
444
445impl PhoneMultiFactorAssertion {
446 pub(crate) fn new(credential: PhoneAuthCredential) -> Self {
447 Self { credential }
448 }
449
450 pub(crate) fn credential(&self) -> &PhoneAuthCredential {
451 &self.credential
452 }
453}
454
455#[derive(Clone, Debug)]
456pub struct TotpSecret {
457 secret_key: String,
458 hashing_algorithm: String,
459 code_length: u32,
460 code_interval_seconds: u32,
461 enrollment_deadline: SystemTime,
462 session_info: String,
463 auth: Weak<Auth>,
464}
465
466impl TotpSecret {
467 pub(crate) fn new(
468 auth: &Arc<Auth>,
469 secret_key: String,
470 hashing_algorithm: String,
471 code_length: u32,
472 code_interval_seconds: u32,
473 enrollment_deadline: SystemTime,
474 session_info: String,
475 ) -> Self {
476 Self {
477 secret_key,
478 hashing_algorithm,
479 code_length,
480 code_interval_seconds,
481 enrollment_deadline,
482 session_info,
483 auth: Arc::downgrade(auth),
484 }
485 }
486
487 pub fn secret_key(&self) -> &str {
488 &self.secret_key
489 }
490
491 pub fn hashing_algorithm(&self) -> &str {
492 &self.hashing_algorithm
493 }
494
495 pub fn code_length(&self) -> u32 {
496 self.code_length
497 }
498
499 pub fn code_interval_seconds(&self) -> u32 {
500 self.code_interval_seconds
501 }
502
503 pub fn enrollment_deadline(&self) -> SystemTime {
504 self.enrollment_deadline
505 }
506
507 pub fn qr_code_url(&self, account_name: Option<&str>, issuer: Option<&str>) -> String {
508 let auth = self.auth.upgrade();
509 let default_account = account_name
510 .filter(|name| !name.is_empty())
511 .map(|value| value.to_string())
512 .or_else(|| {
513 auth.as_ref()
514 .and_then(|auth| auth.current_user())
515 .and_then(|user| user.info().email.clone())
516 })
517 .unwrap_or_else(|| "unknownuser".into());
518 let default_issuer = issuer
519 .filter(|name| !name.is_empty())
520 .map(|value| value.to_string())
521 .or_else(|| auth.as_ref().map(|auth| auth.app().name().to_string()))
522 .unwrap_or_else(|| "firebase".into());
523 let encoded_issuer: String = byte_serialize(default_issuer.as_bytes()).collect();
524 format!(
525 "otpauth://totp/{}:{}?secret={}&issuer={}&algorithm={}&digits={}",
526 default_issuer,
527 default_account,
528 self.secret_key,
529 encoded_issuer,
530 self.hashing_algorithm,
531 self.code_length
532 )
533 }
534
535 pub(crate) fn session_info(&self) -> &str {
536 &self.session_info
537 }
538}
539
540#[derive(Clone, Debug)]
541pub struct TotpMultiFactorAssertion {
542 secret: Option<TotpSecret>,
543 enrollment_id: Option<String>,
544 otp: String,
545}
546
547impl TotpMultiFactorAssertion {
548 pub(crate) fn for_enrollment(secret: TotpSecret, otp: impl Into<String>) -> Self {
549 Self {
550 secret: Some(secret),
551 enrollment_id: None,
552 otp: otp.into(),
553 }
554 }
555
556 pub(crate) fn for_sign_in(enrollment_id: impl Into<String>, otp: impl Into<String>) -> Self {
557 Self {
558 secret: None,
559 enrollment_id: Some(enrollment_id.into()),
560 otp: otp.into(),
561 }
562 }
563
564 pub(crate) fn otp(&self) -> &str {
565 &self.otp
566 }
567
568 pub(crate) fn secret(&self) -> Option<&TotpSecret> {
569 self.secret.as_ref()
570 }
571
572 pub(crate) fn enrollment_id(&self) -> Option<&str> {
573 self.enrollment_id.as_deref()
574 }
575}
576
577#[derive(Clone, Debug)]
578pub enum WebAuthnAssertionKind {
579 SignIn {
580 enrollment_id: String,
581 response: WebAuthnAssertionResponse,
582 },
583 Enrollment {
584 attestation: WebAuthnAttestationResponse,
585 },
586}
587
588#[derive(Clone, Debug)]
589pub struct WebAuthnMultiFactorAssertion {
590 kind: WebAuthnAssertionKind,
591}
592
593impl WebAuthnMultiFactorAssertion {
594 pub fn for_sign_in(
595 enrollment_id: impl Into<String>,
596 response: WebAuthnAssertionResponse,
597 ) -> Self {
598 Self {
599 kind: WebAuthnAssertionKind::SignIn {
600 enrollment_id: enrollment_id.into(),
601 response,
602 },
603 }
604 }
605
606 pub fn for_enrollment(attestation: WebAuthnAttestationResponse) -> Self {
607 Self {
608 kind: WebAuthnAssertionKind::Enrollment { attestation },
609 }
610 }
611
612 pub fn enrollment_id(&self) -> Option<&str> {
613 match &self.kind {
614 WebAuthnAssertionKind::SignIn { enrollment_id, .. } => Some(enrollment_id),
615 WebAuthnAssertionKind::Enrollment { .. } => None,
616 }
617 }
618
619 pub fn response(&self) -> Option<&WebAuthnAssertionResponse> {
620 match &self.kind {
621 WebAuthnAssertionKind::SignIn { response, .. } => Some(response),
622 WebAuthnAssertionKind::Enrollment { .. } => None,
623 }
624 }
625
626 pub fn into_sign_in(self) -> Option<(String, WebAuthnAssertionResponse)> {
627 match self.kind {
628 WebAuthnAssertionKind::SignIn {
629 enrollment_id,
630 response,
631 } => Some((enrollment_id, response)),
632 _ => None,
633 }
634 }
635
636 pub fn attestation(&self) -> Option<&WebAuthnAttestationResponse> {
637 match &self.kind {
638 WebAuthnAssertionKind::Enrollment { attestation } => Some(attestation),
639 _ => None,
640 }
641 }
642
643 pub fn into_attestation(self) -> Option<WebAuthnAttestationResponse> {
644 match self.kind {
645 WebAuthnAssertionKind::Enrollment { attestation } => Some(attestation),
646 _ => None,
647 }
648 }
649}
650
651#[derive(Clone, Debug)]
656pub enum MultiFactorAssertion {
657 Phone(PhoneMultiFactorAssertion),
658 Totp(TotpMultiFactorAssertion),
659 WebAuthn(WebAuthnMultiFactorAssertion),
660}
661
662impl MultiFactorAssertion {
663 pub fn factor_id(&self) -> &'static str {
665 match self {
666 MultiFactorAssertion::Phone(_) => PHONE_PROVIDER_ID,
667 MultiFactorAssertion::Totp(_) => "totp",
668 MultiFactorAssertion::WebAuthn(_) => WEBAUTHN_FACTOR_ID,
669 }
670 }
671
672 pub(crate) fn from_phone_credential(credential: PhoneAuthCredential) -> Self {
673 MultiFactorAssertion::Phone(PhoneMultiFactorAssertion::new(credential))
674 }
675
676 pub(crate) fn from_totp_enrollment(secret: TotpSecret, otp: impl Into<String>) -> Self {
677 MultiFactorAssertion::Totp(TotpMultiFactorAssertion::for_enrollment(secret, otp))
678 }
679
680 pub(crate) fn from_totp_sign_in(
681 enrollment_id: impl Into<String>,
682 otp: impl Into<String>,
683 ) -> Self {
684 MultiFactorAssertion::Totp(TotpMultiFactorAssertion::for_sign_in(enrollment_id, otp))
685 }
686
687 pub(crate) fn from_passkey(
688 enrollment_id: impl Into<String>,
689 response: WebAuthnAssertionResponse,
690 ) -> Self {
691 MultiFactorAssertion::WebAuthn(WebAuthnMultiFactorAssertion::for_sign_in(
692 enrollment_id,
693 response,
694 ))
695 }
696}
697
698pub struct TotpMultiFactorGenerator;
700
701impl TotpMultiFactorGenerator {
702 pub fn assertion_for_enrollment(
703 secret: TotpSecret,
704 otp: impl Into<String>,
705 ) -> MultiFactorAssertion {
706 MultiFactorAssertion::from_totp_enrollment(secret, otp)
707 }
708
709 pub fn assertion_for_sign_in(
710 enrollment_id: impl Into<String>,
711 otp: impl Into<String>,
712 ) -> MultiFactorAssertion {
713 MultiFactorAssertion::from_totp_sign_in(enrollment_id, otp)
714 }
715
716 pub async fn generate_secret(
717 auth: &FirebaseAuth,
718 session: &MultiFactorSession,
719 ) -> AuthResult<TotpSecret> {
720 let inner = auth.inner_arc();
721 inner.start_totp_mfa_enrollment(session).await
722 }
723
724 pub const FACTOR_ID: &'static str = "totp";
725}
726
727pub const WEBAUTHN_FACTOR_ID: &str = "webauthn";
728
729#[derive(Clone, Debug, PartialEq, Eq, Hash)]
734pub enum WebAuthnTransport {
735 Usb,
736 Nfc,
737 Ble,
738 Internal,
739 Cable,
740 Hybrid,
741 Unknown(String),
743}
744
745impl WebAuthnTransport {
746 fn from_raw(value: &str) -> Self {
747 match value {
748 "usb" => WebAuthnTransport::Usb,
749 "nfc" => WebAuthnTransport::Nfc,
750 "ble" => WebAuthnTransport::Ble,
751 "internal" => WebAuthnTransport::Internal,
752 "cable" => WebAuthnTransport::Cable,
753 "hybrid" => WebAuthnTransport::Hybrid,
754 other => WebAuthnTransport::Unknown(other.to_string()),
755 }
756 }
757
758 pub fn as_str(&self) -> &str {
760 match self {
761 WebAuthnTransport::Usb => "usb",
762 WebAuthnTransport::Nfc => "nfc",
763 WebAuthnTransport::Ble => "ble",
764 WebAuthnTransport::Internal => "internal",
765 WebAuthnTransport::Cable => "cable",
766 WebAuthnTransport::Hybrid => "hybrid",
767 WebAuthnTransport::Unknown(value) => value.as_str(),
768 }
769 }
770}
771
772impl fmt::Display for WebAuthnTransport {
773 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
774 f.write_str(self.as_str())
775 }
776}
777
778fn parse_transports(value: &Value) -> Vec<WebAuthnTransport> {
779 value
780 .as_array()
781 .map(|items| {
782 items
783 .iter()
784 .filter_map(|transport| transport.as_str())
785 .map(WebAuthnTransport::from_raw)
786 .collect()
787 })
788 .unwrap_or_default()
789}
790
791#[derive(Clone, Debug, PartialEq, Eq)]
797pub struct WebAuthnCredentialDescriptor {
798 id: String,
799 cred_type: String,
800 transports: Vec<WebAuthnTransport>,
801}
802
803impl WebAuthnCredentialDescriptor {
804 fn from_json(value: &Value) -> Option<Self> {
805 let id = value.get("id")?.as_str()?.to_string();
806 let cred_type = value
807 .get("type")
808 .and_then(|candidate| candidate.as_str())
809 .unwrap_or("public-key")
810 .to_string();
811 let transports = value
812 .get("transports")
813 .map(parse_transports)
814 .unwrap_or_default();
815 Some(Self {
816 id,
817 cred_type,
818 transports,
819 })
820 }
821
822 pub fn id(&self) -> &str {
824 &self.id
825 }
826
827 pub fn credential_type(&self) -> &str {
829 &self.cred_type
830 }
831
832 pub fn transports(&self) -> &[WebAuthnTransport] {
834 &self.transports
835 }
836}
837
838pub struct WebAuthnMultiFactorGenerator;
839
840impl WebAuthnMultiFactorGenerator {
841 pub fn assertion_for_sign_in(
842 enrollment_id: impl Into<String>,
843 response: WebAuthnAssertionResponse,
844 ) -> MultiFactorAssertion {
845 MultiFactorAssertion::from_passkey(enrollment_id, response)
846 }
847
848 pub fn assertion_for_enrollment(
849 attestation: WebAuthnAttestationResponse,
850 ) -> MultiFactorAssertion {
851 MultiFactorAssertion::WebAuthn(WebAuthnMultiFactorAssertion::for_enrollment(attestation))
852 }
853}
854
855#[derive(Clone, Debug)]
856pub struct WebAuthnSignInChallenge {
857 payload: Value,
858}
859
860impl WebAuthnSignInChallenge {
861 pub fn challenge_b64(&self) -> Option<&str> {
863 self.challenge()
864 }
865
866 pub fn challenge_bytes(&self) -> AuthResult<Vec<u8>> {
868 let challenge = self
869 .challenge()
870 .ok_or_else(|| AuthError::InvalidCredential("WebAuthn challenge is missing".into()))?;
871 base64_decode_bytes(challenge).map_err(|_| {
872 AuthError::InvalidCredential("WebAuthn challenge is not valid base64url".into())
873 })
874 }
875
876 pub fn challenge(&self) -> Option<&str> {
877 self.payload
878 .get("challenge")
879 .and_then(|value| value.as_str())
880 }
881
882 pub fn rp_id(&self) -> Option<&str> {
883 self.payload.get("rpId").and_then(|value| value.as_str())
884 }
885
886 pub fn user_handle(&self) -> Option<&str> {
887 self.payload
888 .get("userHandle")
889 .and_then(|value| value.as_str())
890 }
891
892 pub fn allow_credentials(&self) -> Vec<WebAuthnCredentialDescriptor> {
894 self.payload
895 .get("allowCredentials")
896 .and_then(|value| value.as_array())
897 .map(|items| {
898 items
899 .iter()
900 .filter_map(WebAuthnCredentialDescriptor::from_json)
901 .collect()
902 })
903 .unwrap_or_default()
904 }
905
906 pub fn as_raw(&self) -> &Value {
907 &self.payload
908 }
909
910 pub fn into_raw(self) -> Value {
911 self.payload
912 }
913
914 pub fn from_value(value: Value) -> AuthResult<Self> {
915 if value
916 .get("challenge")
917 .and_then(|candidate| candidate.as_str())
918 .is_none()
919 {
920 return Err(AuthError::InvalidCredential(
921 "WebAuthn sign-in challenge is missing a challenge value".into(),
922 ));
923 }
924 Ok(Self { payload: value })
925 }
926}
927
928#[derive(Clone, Debug)]
929pub struct WebAuthnEnrollmentChallenge {
930 payload: Value,
931}
932
933impl WebAuthnEnrollmentChallenge {
934 pub fn challenge_b64(&self) -> Option<&str> {
936 self.challenge()
937 }
938
939 pub fn challenge_bytes(&self) -> AuthResult<Vec<u8>> {
941 let challenge = self
942 .challenge()
943 .ok_or_else(|| AuthError::InvalidCredential("WebAuthn challenge is missing".into()))?;
944 base64_decode_bytes(challenge).map_err(|_| {
945 AuthError::InvalidCredential("WebAuthn challenge is not valid base64url".into())
946 })
947 }
948
949 pub fn challenge(&self) -> Option<&str> {
950 self.payload
951 .get("challenge")
952 .and_then(|value| value.as_str())
953 }
954
955 pub fn rp_id(&self) -> Option<&str> {
956 self.payload.get("rpId").and_then(|value| value.as_str())
957 }
958
959 pub fn user_name(&self) -> Option<&str> {
960 self.payload
961 .get("user")
962 .and_then(|user| user.get("name"))
963 .and_then(|value| value.as_str())
964 }
965
966 pub fn user_id(&self) -> Option<&[u8]> {
967 self.payload
968 .get("user")
969 .and_then(|user| user.get("id"))
970 .and_then(|value| value.as_str())
971 .map(|encoded| encoded.as_bytes())
972 }
973
974 pub fn as_raw(&self) -> &Value {
975 &self.payload
976 }
977
978 pub fn into_raw(self) -> Value {
979 self.payload
980 }
981
982 pub fn from_value(value: Value) -> AuthResult<Self> {
983 if value
984 .get("challenge")
985 .and_then(|candidate| candidate.as_str())
986 .is_none()
987 {
988 return Err(AuthError::InvalidCredential(
989 "WebAuthn enrollment challenge is missing a challenge value".into(),
990 ));
991 }
992 Ok(Self { payload: value })
993 }
994}
995
996#[derive(Clone, Debug)]
997pub struct WebAuthnAssertionResponse {
998 payload: Value,
999}
1000
1001impl WebAuthnAssertionResponse {
1002 pub fn with_authenticator_data(mut self, data: impl Into<String>) -> Self {
1007 self.set_field("authenticatorData", Value::String(data.into()));
1008 self
1009 }
1010
1011 pub fn with_signature(mut self, signature: impl Into<String>) -> Self {
1013 self.set_field("signature", Value::String(signature.into()));
1014 self
1015 }
1016
1017 pub fn with_user_handle(mut self, user_handle: impl Into<Option<String>>) -> Self {
1019 if let Value::Object(ref mut map) = self.payload {
1020 match user_handle.into() {
1021 Some(value) => {
1022 map.insert("userHandle".to_string(), Value::String(value));
1023 }
1024 None => {
1025 map.remove("userHandle");
1026 }
1027 }
1028 }
1029 self
1030 }
1031
1032 pub fn credential_id(&self) -> Option<&str> {
1033 self.payload
1034 .get("credentialId")
1035 .and_then(|value| value.as_str())
1036 }
1037
1038 pub fn client_data_json(&self) -> Option<&str> {
1039 self.payload
1040 .get("clientDataJSON")
1041 .and_then(|value| value.as_str())
1042 }
1043
1044 pub fn authenticator_data(&self) -> Option<&str> {
1046 self.payload
1047 .get("authenticatorData")
1048 .and_then(|value| value.as_str())
1049 }
1050
1051 pub fn signature(&self) -> Option<&str> {
1053 self.payload
1054 .get("signature")
1055 .and_then(|value| value.as_str())
1056 }
1057
1058 pub fn user_handle(&self) -> Option<&str> {
1060 self.payload
1061 .get("userHandle")
1062 .and_then(|value| value.as_str())
1063 }
1064
1065 fn set_field(&mut self, key: &str, value: Value) {
1066 if let Value::Object(ref mut map) = self.payload {
1067 map.insert(key.to_string(), value);
1068 }
1069 }
1070
1071 pub fn as_raw(&self) -> &Value {
1072 &self.payload
1073 }
1074
1075 pub fn into_raw(self) -> Value {
1076 self.payload
1077 }
1078}
1079
1080#[derive(Clone, Debug)]
1081pub struct WebAuthnAttestationResponse {
1082 payload: Value,
1083}
1084
1085impl WebAuthnAttestationResponse {
1086 pub fn with_attestation_object(mut self, payload: impl Into<String>) -> Self {
1088 self.set_field("attestationObject", Value::String(payload.into()));
1089 self
1090 }
1091
1092 pub fn with_credential_public_key(mut self, payload: impl Into<String>) -> Self {
1094 self.set_field("credentialPublicKey", Value::String(payload.into()));
1095 self
1096 }
1097
1098 pub fn with_transports<I, T>(mut self, transports: I) -> Self
1100 where
1101 I: IntoIterator<Item = T>,
1102 T: Into<String>,
1103 {
1104 let values = transports
1105 .into_iter()
1106 .map(|value| Value::String(value.into()))
1107 .collect();
1108 self.set_field("transports", Value::Array(values));
1109 self
1110 }
1111
1112 pub fn credential_id(&self) -> Option<&str> {
1113 self.payload
1114 .get("credentialId")
1115 .and_then(|value| value.as_str())
1116 }
1117
1118 pub fn client_data_json(&self) -> Option<&str> {
1119 self.payload
1120 .get("clientDataJSON")
1121 .and_then(|value| value.as_str())
1122 }
1123
1124 pub fn attestation_object(&self) -> Option<&str> {
1126 self.payload
1127 .get("attestationObject")
1128 .and_then(|value| value.as_str())
1129 }
1130
1131 pub fn credential_public_key(&self) -> Option<&str> {
1133 self.payload
1134 .get("credentialPublicKey")
1135 .and_then(|value| value.as_str())
1136 }
1137
1138 pub fn transports(&self) -> Vec<WebAuthnTransport> {
1140 self.payload
1141 .get("transports")
1142 .map(parse_transports)
1143 .unwrap_or_default()
1144 }
1145
1146 fn set_field(&mut self, key: &str, value: Value) {
1147 if let Value::Object(ref mut map) = self.payload {
1148 map.insert(key.to_string(), value);
1149 }
1150 }
1151
1152 pub fn as_raw(&self) -> &Value {
1153 &self.payload
1154 }
1155
1156 pub fn into_raw(self) -> Value {
1157 self.payload
1158 }
1159}
1160
1161impl TryFrom<Value> for WebAuthnAttestationResponse {
1162 type Error = AuthError;
1163
1164 fn try_from(value: Value) -> Result<Self, Self::Error> {
1165 let credential_present = value
1166 .get("credentialId")
1167 .and_then(|candidate| candidate.as_str())
1168 .is_some();
1169 let attestation_present = value
1170 .get("attestationObject")
1171 .and_then(|candidate| candidate.as_str())
1172 .is_some();
1173 let client_data_present = value
1174 .get("clientDataJSON")
1175 .and_then(|candidate| candidate.as_str())
1176 .is_some();
1177
1178 if credential_present && attestation_present && client_data_present {
1179 Ok(Self { payload: value })
1180 } else {
1181 Err(AuthError::InvalidCredential(
1182 "WebAuthn registration payload missing required fields".into(),
1183 ))
1184 }
1185 }
1186}
1187
1188impl TryFrom<Value> for WebAuthnAssertionResponse {
1189 type Error = AuthError;
1190
1191 fn try_from(value: Value) -> Result<Self, Self::Error> {
1192 let credential_present = value
1193 .get("credentialId")
1194 .and_then(|candidate| candidate.as_str())
1195 .is_some();
1196 let client_data_present = value
1197 .get("clientDataJSON")
1198 .and_then(|candidate| candidate.as_str())
1199 .is_some();
1200
1201 if credential_present && client_data_present {
1202 Ok(Self { payload: value })
1203 } else {
1204 Err(AuthError::InvalidCredential(
1205 "WebAuthn verification payload missing credentialId or clientDataJSON".into(),
1206 ))
1207 }
1208 }
1209}
1210
1211#[derive(Clone, Debug)]
1212pub struct MultiFactorError {
1213 operation: MultiFactorOperation,
1214 hints: Vec<MultiFactorInfo>,
1215 session: MultiFactorSession,
1216 context: Arc<MultiFactorSignInContext>,
1217 user: Option<Arc<User>>,
1218}
1219
1220impl MultiFactorError {
1221 pub(crate) fn new(
1222 operation: MultiFactorOperation,
1223 session: MultiFactorSession,
1224 hints: Vec<MultiFactorInfo>,
1225 context: MultiFactorSignInContext,
1226 user: Option<Arc<User>>,
1227 ) -> Self {
1228 Self {
1229 operation,
1230 hints,
1231 session,
1232 context: Arc::new(context),
1233 user,
1234 }
1235 }
1236
1237 pub fn hints(&self) -> &[MultiFactorInfo] {
1239 &self.hints
1240 }
1241
1242 pub fn session(&self) -> &MultiFactorSession {
1244 &self.session
1245 }
1246
1247 pub fn operation(&self) -> MultiFactorOperation {
1249 self.operation
1250 }
1251
1252 pub(crate) fn context(&self) -> Arc<MultiFactorSignInContext> {
1253 Arc::clone(&self.context)
1254 }
1255
1256 pub(crate) fn user(&self) -> Option<Arc<User>> {
1257 self.user.clone()
1258 }
1259}
1260
1261impl fmt::Display for MultiFactorError {
1262 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1263 match self.operation {
1264 MultiFactorOperation::SignIn => write!(f, "Multi-factor sign-in required"),
1265 MultiFactorOperation::Reauthenticate => {
1266 write!(f, "Multi-factor reauthentication required")
1267 }
1268 MultiFactorOperation::Link => {
1269 write!(f, "Multi-factor linking required")
1270 }
1271 }
1272 }
1273}
1274
1275#[derive(Clone)]
1276pub struct MultiFactorResolver {
1277 auth: Arc<Auth>,
1278 hints: Vec<MultiFactorInfo>,
1279 session: MultiFactorSession,
1280 operation: MultiFactorOperation,
1281 context: Arc<MultiFactorSignInContext>,
1282 _user: Option<Arc<User>>,
1283}
1284
1285impl MultiFactorResolver {
1286 pub(crate) fn from_error(auth: Arc<Auth>, error: MultiFactorError) -> Self {
1287 Self {
1288 hints: error.hints.clone(),
1289 session: error.session.clone(),
1290 operation: error.operation(),
1291 context: error.context(),
1292 _user: error.user(),
1293 auth,
1294 }
1295 }
1296
1297 pub fn hints(&self) -> &[MultiFactorInfo] {
1299 &self.hints
1300 }
1301
1302 pub fn session(&self) -> &MultiFactorSession {
1304 &self.session
1305 }
1306
1307 pub async fn send_phone_sign_in_code(
1309 &self,
1310 hint: &MultiFactorInfo,
1311 verifier: Arc<dyn ApplicationVerifier>,
1312 ) -> AuthResult<String> {
1313 let pending = self.session.pending_credential().ok_or_else(|| {
1314 AuthError::InvalidCredential(
1315 "Multi-factor session is not valid for challenge resolution".into(),
1316 )
1317 })?;
1318
1319 self.auth
1320 .start_phone_multi_factor_sign_in(pending, &hint.uid, verifier)
1321 .await
1322 }
1323
1324 pub async fn start_passkey_sign_in(
1326 &self,
1327 hint: &MultiFactorInfo,
1328 ) -> AuthResult<WebAuthnSignInChallenge> {
1329 if hint.factor_id != WEBAUTHN_FACTOR_ID {
1330 return Err(AuthError::InvalidCredential(
1331 "Hint does not reference a WebAuthn factor".into(),
1332 ));
1333 }
1334
1335 let pending = self.session.pending_credential().ok_or_else(|| {
1336 AuthError::InvalidCredential(
1337 "Multi-factor session is not valid for challenge resolution".into(),
1338 )
1339 })?;
1340
1341 self.auth
1342 .start_passkey_multi_factor_sign_in(pending, &hint.uid)
1343 .await
1344 }
1345
1346 pub async fn resolve_sign_in(
1348 &self,
1349 assertion: MultiFactorAssertion,
1350 ) -> AuthResult<UserCredential> {
1351 let pending = self.session.pending_credential().ok_or_else(|| {
1352 AuthError::InvalidCredential(
1353 "Multi-factor session is not valid for challenge resolution".into(),
1354 )
1355 })?;
1356
1357 match assertion {
1358 MultiFactorAssertion::Phone(assertion) => {
1359 let verification_id = assertion.credential().verification_id();
1360 let verification_code = assertion.credential().verification_code();
1361
1362 self.auth
1363 .finalize_phone_multi_factor_sign_in(
1364 pending,
1365 verification_id,
1366 verification_code,
1367 Arc::clone(&self.context),
1368 self.operation,
1369 )
1370 .await
1371 }
1372 MultiFactorAssertion::Totp(assertion) => {
1373 let enrollment_id = assertion.enrollment_id().ok_or_else(|| {
1374 AuthError::InvalidCredential(
1375 "TOTP assertions require an enrollment identifier".into(),
1376 )
1377 })?;
1378
1379 self.auth
1380 .finalize_totp_multi_factor_sign_in(
1381 pending,
1382 enrollment_id,
1383 assertion.otp(),
1384 Arc::clone(&self.context),
1385 self.operation,
1386 )
1387 .await
1388 }
1389 MultiFactorAssertion::WebAuthn(assertion) => {
1390 let (enrollment_id, response) = assertion.into_sign_in().ok_or_else(|| {
1391 AuthError::InvalidCredential(
1392 "WebAuthn assertion is not valid for sign-in".to_string(),
1393 )
1394 })?;
1395 self.auth
1396 .finalize_passkey_multi_factor_sign_in(
1397 pending,
1398 &enrollment_id,
1399 response,
1400 Arc::clone(&self.context),
1401 self.operation,
1402 )
1403 .await
1404 }
1405 }
1406 }
1407}
1408
1409#[derive(Clone)]
1410pub struct MultiFactorUser {
1411 auth: Arc<Auth>,
1412}
1413
1414impl MultiFactorUser {
1415 pub(crate) fn new(auth: Arc<Auth>) -> Self {
1416 Self { auth }
1417 }
1418
1419 pub async fn enrolled_factors(&self) -> AuthResult<Vec<MultiFactorInfo>> {
1421 self.auth.fetch_enrolled_factors().await
1422 }
1423
1424 pub async fn get_session(&self) -> AuthResult<MultiFactorSession> {
1426 self.auth.multi_factor_session().await
1427 }
1428
1429 pub async fn generate_totp_secret(
1431 &self,
1432 session: &MultiFactorSession,
1433 ) -> AuthResult<TotpSecret> {
1434 self.auth.start_totp_mfa_enrollment(session).await
1435 }
1436
1437 pub async fn enroll(
1439 &self,
1440 session: &MultiFactorSession,
1441 assertion: MultiFactorAssertion,
1442 display_name: Option<&str>,
1443 ) -> AuthResult<UserCredential> {
1444 match assertion {
1445 MultiFactorAssertion::Totp(assertion) => {
1446 if session.session_type() != MultiFactorSessionType::Enrollment {
1447 return Err(AuthError::InvalidCredential(
1448 "TOTP enrollment requires an enrollment session".into(),
1449 ));
1450 }
1451 let id_token = session.id_token().ok_or_else(|| {
1452 AuthError::InvalidCredential("Missing ID token for enrollment".into())
1453 })?;
1454 let secret = assertion.secret().ok_or_else(|| {
1455 AuthError::InvalidCredential(
1456 "TOTP enrollment assertions require a generated secret".into(),
1457 )
1458 })?;
1459 self.auth
1460 .complete_totp_mfa_enrollment(id_token, secret, assertion.otp(), display_name)
1461 .await
1462 }
1463 MultiFactorAssertion::WebAuthn(assertion) => {
1464 if session.session_type() != MultiFactorSessionType::Enrollment {
1465 return Err(AuthError::InvalidCredential(
1466 "WebAuthn enrollment requires an enrollment session".into(),
1467 ));
1468 }
1469
1470 let attestation = assertion.into_attestation().ok_or_else(|| {
1471 AuthError::InvalidCredential(
1472 "WebAuthn enrollment assertions require an attestation payload".to_string(),
1473 )
1474 })?;
1475
1476 self.auth
1477 .complete_passkey_mfa_enrollment(session, attestation, display_name)
1478 .await
1479 }
1480 _ => Err(AuthError::NotImplemented(
1481 "Only TOTP and WebAuthn assertions are supported via MultiFactorUser::enroll",
1482 )),
1483 }
1484 }
1485
1486 pub async fn start_passkey_enrollment(
1488 &self,
1489 session: &MultiFactorSession,
1490 ) -> AuthResult<WebAuthnEnrollmentChallenge> {
1491 self.auth.start_passkey_mfa_enrollment(session).await
1492 }
1493
1494 pub async fn enroll_phone_number(
1496 &self,
1497 phone_number: &str,
1498 verifier: Arc<dyn ApplicationVerifier>,
1499 display_name: Option<&str>,
1500 ) -> AuthResult<ConfirmationResult> {
1501 self.auth
1502 .start_phone_mfa_enrollment(phone_number, verifier, display_name)
1503 .await
1504 }
1505
1506 pub async fn unenroll(&self, factor_uid: &str) -> AuthResult<()> {
1508 self.auth.withdraw_multi_factor(factor_uid).await
1509 }
1510}
1511
1512#[derive(Clone)]
1513pub struct AuthStateListener {
1514 pub observer: PartialObserver<Arc<User>>,
1515}
1516
1517impl AuthStateListener {
1518 pub fn new(observer: PartialObserver<Arc<User>>) -> Self {
1520 Self { observer }
1521 }
1522}
1523
1524pub type Observer<T> = PartialObserver<T>;
1525
1526#[derive(Clone)]
1527pub struct FirebaseAuth {
1528 inner: Arc<Auth>,
1529}
1530
1531impl FirebaseAuth {
1532 pub fn new(inner: Arc<Auth>) -> Self {
1534 Self { inner }
1535 }
1536
1537 pub fn app(&self) -> &FirebaseApp {
1539 self.inner.app()
1540 }
1541
1542 pub fn current_user(&self) -> Option<Arc<User>> {
1544 self.inner.current_user()
1545 }
1546
1547 pub fn sign_out(&self) {
1549 self.inner.sign_out();
1550 }
1551
1552 pub(crate) fn inner_arc(&self) -> Arc<Auth> {
1553 self.inner.clone()
1554 }
1555
1556 pub async fn sign_in_with_email_and_password(
1558 &self,
1559 email: &str,
1560 password: &str,
1561 ) -> AuthResult<UserCredential> {
1562 self.inner
1563 .sign_in_with_email_and_password(email, password)
1564 .await
1565 }
1566
1567 pub async fn create_user_with_email_and_password(
1569 &self,
1570 email: &str,
1571 password: &str,
1572 ) -> AuthResult<UserCredential> {
1573 self.inner
1574 .create_user_with_email_and_password(email, password)
1575 .await
1576 }
1577
1578 pub fn on_auth_state_changed(
1580 &self,
1581 observer: PartialObserver<Arc<User>>,
1582 ) -> impl FnOnce() + Send + 'static {
1583 self.inner.on_auth_state_changed(observer)
1584 }
1585}
1586
1587pub fn get_multi_factor_resolver(
1591 auth: &FirebaseAuth,
1592 error: &AuthError,
1593) -> AuthResult<MultiFactorResolver> {
1594 match error {
1595 AuthError::MultiFactorRequired(mfa_error) => Ok(MultiFactorResolver::from_error(
1596 auth.inner_arc(),
1597 mfa_error.clone(),
1598 )),
1599 _ => Err(AuthError::InvalidCredential(
1600 "The supplied error does not contain multi-factor context".into(),
1601 )),
1602 }
1603}
1604#[cfg(test)]
1605mod tests {
1606 use super::*;
1607 use crate::auth::error::AuthError;
1608 use serde_json::json;
1609
1610 #[tokio::test(flavor = "current_thread")]
1611 async fn confirmation_result_invokes_handler() {
1612 let result = ConfirmationResult::new("verification_id".into(), |code| {
1613 let code = code.to_string();
1614 async move {
1615 assert_eq!(code, "123456");
1616 Err(AuthError::NotImplemented("test"))
1617 }
1618 });
1619 assert!(result.confirm("123456").await.is_err());
1620 }
1621
1622 #[test]
1623 fn webauthn_sign_in_challenge_accessors() {
1624 let payload = json!({
1625 "challenge": "QUJD",
1626 "rpId": "example.com",
1627 "userHandle": "user-handle",
1628 "allowCredentials": [
1629 {
1630 "type": "public-key",
1631 "id": "cred-1",
1632 "transports": ["usb", "internal"]
1633 },
1634 {
1635 "type": "public-key",
1636 "id": "cred-2"
1637 }
1638 ]
1639 });
1640
1641 let challenge = WebAuthnSignInChallenge::from_value(payload).expect("valid challenge");
1642 let allow = challenge.allow_credentials();
1643 assert_eq!(allow.len(), 2);
1644 assert_eq!(allow[0].id(), "cred-1");
1645 assert_eq!(allow[0].credential_type(), "public-key");
1646 assert_eq!(
1647 allow[0].transports(),
1648 &[WebAuthnTransport::Usb, WebAuthnTransport::Internal]
1649 );
1650 assert_eq!(allow[1].id(), "cred-2");
1651 assert!(allow[1].transports().is_empty());
1652 assert_eq!(challenge.user_handle(), Some("user-handle"));
1653 let decoded = challenge.challenge_bytes().expect("decoded challenge");
1654 assert_eq!(decoded, b"ABC");
1655 }
1656
1657 #[test]
1658 fn webauthn_attestation_response_accessors() {
1659 let payload = json!({
1660 "credentialId": "cred-123",
1661 "clientDataJSON": "BASE64CLIENT",
1662 "attestationObject": "ATTEST",
1663 "credentialPublicKey": "PUBKEY",
1664 "transports": ["nfc", "unknown"]
1665 });
1666
1667 let response = WebAuthnAttestationResponse::try_from(payload).expect("attestation");
1668 assert_eq!(response.credential_id(), Some("cred-123"));
1669 assert_eq!(response.client_data_json(), Some("BASE64CLIENT"));
1670 assert_eq!(response.attestation_object(), Some("ATTEST"));
1671 assert_eq!(response.credential_public_key(), Some("PUBKEY"));
1672 let transports = response.transports();
1673 assert_eq!(transports.len(), 2);
1674 assert_eq!(transports[0], WebAuthnTransport::Nfc);
1675 assert_eq!(transports[1].as_str(), "unknown");
1676
1677 let updated = response
1678 .clone()
1679 .with_attestation_object("UPDATED")
1680 .with_transports(["internal", "usb"])
1681 .with_credential_public_key("NEWKEY");
1682 assert_eq!(updated.attestation_object(), Some("UPDATED"));
1683 assert_eq!(updated.credential_public_key(), Some("NEWKEY"));
1684 assert_eq!(
1685 updated.transports(),
1686 vec![WebAuthnTransport::Internal, WebAuthnTransport::Usb]
1687 );
1688 }
1689
1690 #[test]
1691 fn webauthn_assertion_response_accessors() {
1692 let payload = json!({
1693 "credentialId": "cred-abc",
1694 "clientDataJSON": "CLIENT",
1695 "authenticatorData": "AUTH_DATA",
1696 "signature": "SIG",
1697 "userHandle": "USER"
1698 });
1699
1700 let response = WebAuthnAssertionResponse::try_from(payload).expect("assertion");
1701 assert_eq!(response.credential_id(), Some("cred-abc"));
1702 assert_eq!(response.client_data_json(), Some("CLIENT"));
1703 assert_eq!(response.authenticator_data(), Some("AUTH_DATA"));
1704 assert_eq!(response.signature(), Some("SIG"));
1705 assert_eq!(response.user_handle(), Some("USER"));
1706
1707 let updated = response
1708 .clone()
1709 .with_signature("NEW_SIG")
1710 .with_authenticator_data("NEW_AUTH")
1711 .with_user_handle(None)
1712 .with_user_handle(Some("ALICE".to_string()));
1713 assert_eq!(updated.signature(), Some("NEW_SIG"));
1714 assert_eq!(updated.authenticator_data(), Some("NEW_AUTH"));
1715 assert_eq!(updated.user_handle(), Some("ALICE"));
1716 }
1717
1718 #[test]
1719 fn webauthn_enrollment_challenge_decodes_bytes() {
1720 let payload = json!({
1721 "challenge": "QUJDRA",
1722 "rpId": "example.com"
1723 });
1724
1725 let challenge = WebAuthnEnrollmentChallenge::from_value(payload).expect("challenge");
1726 let decoded = challenge.challenge_bytes().expect("decoded");
1727 assert_eq!(decoded, b"ABCD");
1728 }
1729}