firebase_rs_sdk/auth/
types.rs

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    /// Returns the requestType string expected by the Identity Toolkit API.
74    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    /// Parses a `requestType` string returned by the REST API.
86    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    /// Parses the `mode` query parameter from action code links.
101    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    /// Parses an out-of-band action link into its structured representation.
140    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    /// Creates a confirmation result that can complete sign-in with the provided handler.
220    #[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    /// Creates a confirmation result that can complete sign-in with the provided handler.
237    #[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    /// Finalizes authentication by providing the SMS verification code.
254    pub async fn confirm(&self, verification_code: &str) -> AuthResult<UserCredential> {
255        (self.confirm_handler)(verification_code).await
256    }
257
258    /// Returns the verification ID that should be paired with the SMS code.
259    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/// Distinguishes between enrollment and sign-in multi-factor sessions.
331#[derive(Clone, Copy, Debug, PartialEq, Eq)]
332pub enum MultiFactorSessionType {
333    /// A session captured during enrollment flows (contains an ID token).
334    Enrollment,
335    /// A session captured during sign-in flows (contains an MFA pending credential).
336    SignIn,
337}
338
339/// Indicates which primary flow triggered a multi-factor requirement.
340#[derive(Clone, Copy, Debug, PartialEq, Eq)]
341pub enum MultiFactorOperation {
342    /// Multi-factor is required while completing a sign-in.
343    SignIn,
344    /// Multi-factor is required while completing a reauthentication.
345    Reauthenticate,
346    /// Multi-factor is required while completing a link operation.
347    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    /// Returns the raw credential captured for this session.
372    ///
373    /// For enrollment sessions this is the user's ID token, while for sign-in sessions it represents
374    /// the `mfaPendingCredential` returned by the server.
375    pub fn credential(&self) -> &str {
376        &self.credential
377    }
378
379    /// Returns the type of multi-factor session that was established.
380    pub fn session_type(&self) -> MultiFactorSessionType {
381        self.kind
382    }
383
384    /// Returns the ID token captured for enrollment sessions.
385    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    /// Returns the pending credential captured for sign-in sessions.
393    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/// A multi-factor assertion that can be resolved to complete sign-in.
652///
653/// Mirrors the behaviour of the JavaScript `MultiFactorAssertion` found in
654/// `packages/auth/src/mfa/mfa_assertion.ts`.
655#[derive(Clone, Debug)]
656pub enum MultiFactorAssertion {
657    Phone(PhoneMultiFactorAssertion),
658    Totp(TotpMultiFactorAssertion),
659    WebAuthn(WebAuthnMultiFactorAssertion),
660}
661
662impl MultiFactorAssertion {
663    /// Returns the identifier of the underlying second factor.
664    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
698/// Builder for time-based one-time password multi-factor assertions.
699pub 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/// The transport mechanisms declared for a WebAuthn credential.
730///
731/// Mirrors the identifiers returned by the WebAuthn browser APIs and exposed by the Firebase
732/// JS SDK (`packages/auth/src/model/public_types.ts`).
733#[derive(Clone, Debug, PartialEq, Eq, Hash)]
734pub enum WebAuthnTransport {
735    Usb,
736    Nfc,
737    Ble,
738    Internal,
739    Cable,
740    Hybrid,
741    /// A transport identifier that is not yet known to this SDK.
742    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    /// Returns the string identifier used by the underlying WebAuthn API.
759    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/// Descriptor describing a credential that can satisfy a WebAuthn challenge.
792///
793/// Follows the shape returned by the Identity Toolkit API and exposed by the modular JS SDK
794/// (`PublicKeyCredentialDescriptor` in
795/// `packages/auth/src/model/public_types.ts`).
796#[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    /// Returns the base64url encoded credential identifier.
823    pub fn id(&self) -> &str {
824        &self.id
825    }
826
827    /// Returns the credential type (usually `"public-key"`).
828    pub fn credential_type(&self) -> &str {
829        &self.cred_type
830    }
831
832    /// Returns the transports declared for this credential.
833    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    /// Returns the base64url-encoded challenge exactly as sent by the backend.
862    pub fn challenge_b64(&self) -> Option<&str> {
863        self.challenge()
864    }
865
866    /// Decodes the challenge into raw bytes using the same URL-safe alphabet as the JS SDK.
867    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    /// Returns the credential descriptors that can satisfy this challenge.
893    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    /// Returns the base64url-encoded challenge exactly as sent by the backend.
935    pub fn challenge_b64(&self) -> Option<&str> {
936        self.challenge()
937    }
938
939    /// Decodes the challenge into raw bytes using the same URL-safe alphabet as the JS SDK.
940    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    /// Returns a new response with the provided authenticator data encoded as base64url.
1003    ///
1004    /// Mirrors the helpers on the JS `MultiFactorAssertionImpl` in
1005    /// `packages/auth/src/mfa/mfa_assertion.ts` that expose mutators for the browser payload.
1006    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    /// Returns a new response with the provided signature encoded as base64url.
1012    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    /// Returns a new response with the provided optional user handle.
1018    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    /// Returns the authenticator data associated with the assertion response.
1045    pub fn authenticator_data(&self) -> Option<&str> {
1046        self.payload
1047            .get("authenticatorData")
1048            .and_then(|value| value.as_str())
1049    }
1050
1051    /// Returns the raw signature produced by the authenticator, base64url encoded.
1052    pub fn signature(&self) -> Option<&str> {
1053        self.payload
1054            .get("signature")
1055            .and_then(|value| value.as_str())
1056    }
1057
1058    /// Returns the optional user handle provided by the authenticator.
1059    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    /// Returns a new response with the provided attestation object encoded as base64url.
1087    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    /// Returns a new response with the provided credential public key.
1093    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    /// Returns a new response with the provided transports.
1099    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    /// Returns the attestation object emitted by the authenticator, base64url encoded.
1125    pub fn attestation_object(&self) -> Option<&str> {
1126        self.payload
1127            .get("attestationObject")
1128            .and_then(|value| value.as_str())
1129    }
1130
1131    /// Returns the credential public key when provided by the platform authenticator.
1132    pub fn credential_public_key(&self) -> Option<&str> {
1133        self.payload
1134            .get("credentialPublicKey")
1135            .and_then(|value| value.as_str())
1136    }
1137
1138    /// Returns the transports declared by the authenticator for the newly enrolled credential.
1139    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    /// Returns the factors that can satisfy the pending challenge.
1238    pub fn hints(&self) -> &[MultiFactorInfo] {
1239        &self.hints
1240    }
1241
1242    /// Returns the captured multi-factor session information.
1243    pub fn session(&self) -> &MultiFactorSession {
1244        &self.session
1245    }
1246
1247    /// Returns the operation that triggered the multi-factor requirement.
1248    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    /// Returns the list of factor hints that can satisfy the pending challenge.
1298    pub fn hints(&self) -> &[MultiFactorInfo] {
1299        &self.hints
1300    }
1301
1302    /// Returns the session associated with the pending multi-factor challenge.
1303    pub fn session(&self) -> &MultiFactorSession {
1304        &self.session
1305    }
1306
1307    /// Initiates a phone-based multi-factor challenge for the provided hint.
1308    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    /// Initiates a passkey/WebAuthn challenge for the provided factor hint.
1325    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    /// Resolves the pending multi-factor challenge using the supplied assertion.
1347    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    /// Returns the list of enrolled multi-factor authenticators.
1420    pub async fn enrolled_factors(&self) -> AuthResult<Vec<MultiFactorInfo>> {
1421        self.auth.fetch_enrolled_factors().await
1422    }
1423
1424    /// Requests a multi-factor session for subsequent operations.
1425    pub async fn get_session(&self) -> AuthResult<MultiFactorSession> {
1426        self.auth.multi_factor_session().await
1427    }
1428
1429    /// Generates a TOTP enrollment secret for the provided session.
1430    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    /// Completes enrollment using a multi-factor assertion (e.g. TOTP).
1438    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    /// Starts a WebAuthn/passkey enrollment challenge.
1487    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    /// Starts phone number enrollment by sending a verification SMS.
1495    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    /// Removes an enrolled multi-factor authenticator.
1507    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    /// Wraps an observer so it can be registered with the Auth state machine.
1519    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    /// Creates a high-level Auth façade around the shared `Auth` core.
1533    pub fn new(inner: Arc<Auth>) -> Self {
1534        Self { inner }
1535    }
1536
1537    /// Returns the `FirebaseApp` associated with this Auth instance.
1538    pub fn app(&self) -> &FirebaseApp {
1539        self.inner.app()
1540    }
1541
1542    /// Returns the currently signed-in user, if any.
1543    pub fn current_user(&self) -> Option<Arc<User>> {
1544        self.inner.current_user()
1545    }
1546
1547    /// Signs the current user out of Firebase Auth.
1548    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    /// Signs a user in with an email and password.
1557    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    /// Creates a new user with the provided email and password.
1568    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    /// Registers an observer that is notified whenever the auth state changes.
1579    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
1587/// Returns a [`MultiFactorResolver`] that can be used to complete a pending multi-factor flow.
1588///
1589/// Mirrors the JavaScript helper exported from `packages/auth/src/mfa/mfa_resolver.ts`.
1590pub 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}