firebase_rs_sdk/auth/api/core/
mod.rs

1use std::cmp::Ordering as CmpOrdering;
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::sync::{Arc, Mutex, Weak};
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use reqwest::Client;
7use reqwest::Url;
8use serde::Serialize;
9
10mod account;
11mod idp;
12mod mfa;
13mod phone;
14mod token;
15
16// Re-export for public use
17pub(crate) use token::DEFAULT_SECURE_TOKEN_ENDPOINT;
18pub use token::{refresh_id_token, refresh_id_token_with_endpoint, RefreshTokenResponse};
19
20use crate::app::{register_component, AppError, FirebaseApp, LOGGER as APP_LOGGER};
21use crate::auth::error::{AuthError, AuthResult};
22use crate::auth::model::MfaEnrollmentInfo;
23use crate::auth::model::{
24    AuthConfig, AuthCredential, AuthStateListeners, EmailAuthProvider, GetAccountInfoResponse,
25    SignInWithCustomTokenRequest, SignInWithCustomTokenResponse, SignInWithEmailLinkRequest,
26    SignInWithEmailLinkResponse, SignInWithPasswordRequest, SignInWithPasswordResponse,
27    SignUpRequest, SignUpResponse, User, UserCredential, UserInfo,
28};
29#[cfg(all(
30    feature = "wasm-web",
31    feature = "experimental-indexed-db",
32    target_arch = "wasm32"
33))]
34use crate::auth::persistence::indexed_db::IndexedDbPersistence;
35use crate::auth::persistence::{
36    AuthPersistence, InMemoryPersistence, PersistedAuthState, PersistenceListener,
37    PersistenceSubscription,
38};
39use crate::auth::types::{
40    ActionCodeInfo, ActionCodeInfoData, ActionCodeOperation, ActionCodeSettings, ActionCodeUrl,
41    ApplicationVerifier, ConfirmationResult, MultiFactorError, MultiFactorInfo,
42    MultiFactorOperation, MultiFactorSession, MultiFactorSessionType, MultiFactorSignInContext,
43    MultiFactorUser, TotpSecret, WebAuthnAssertionResponse, WebAuthnAttestationResponse,
44    WebAuthnEnrollmentChallenge, WebAuthnSignInChallenge, WEBAUTHN_FACTOR_ID,
45};
46use crate::auth::{
47    InMemoryRedirectPersistence, OAuthCredential, OAuthPopupHandler, OAuthRedirectHandler,
48    PendingRedirectEvent, RedirectOperation, RedirectPersistence,
49};
50use crate::auth::{PhoneAuthCredential, PHONE_PROVIDER_ID};
51use crate::component::types::{
52    ComponentError, DynService, InstanceFactoryOptions, InstantiationMode,
53};
54use crate::component::{Component, ComponentContainer, ComponentType};
55//#[cfg(feature = "firestore")]
56use crate::firestore::TokenProviderArc;
57use crate::platform::runtime::{sleep as runtime_sleep, spawn_detached};
58use crate::platform::token::{AsyncTokenProvider, TokenError};
59use crate::util::PartialObserver;
60use account::{
61    apply_action_code, confirm_password_reset, delete_account, get_account_info,
62    reset_password_info, send_email_verification, send_password_reset_email,
63    send_sign_in_link_to_email, update_account, verify_password, UpdateAccountRequest,
64    UpdateAccountResponse, UpdateString,
65};
66use idp::{sign_in_with_idp, SignInWithIdpRequest, SignInWithIdpResponse};
67use mfa::{
68    finalize_passkey_mfa_enrollment, finalize_passkey_mfa_sign_in, finalize_phone_mfa_enrollment,
69    finalize_phone_mfa_sign_in, finalize_totp_mfa_enrollment, finalize_totp_mfa_sign_in,
70    start_passkey_mfa_enrollment, start_passkey_mfa_sign_in, start_phone_mfa_enrollment,
71    start_phone_mfa_sign_in, start_totp_mfa_enrollment, withdraw_mfa,
72    FinalizePasskeyMfaEnrollmentRequest, FinalizePasskeyMfaSignInRequest,
73    FinalizePhoneMfaEnrollmentRequest, FinalizePhoneMfaSignInRequest,
74    FinalizeTotpMfaEnrollmentRequest, FinalizeTotpMfaSignInRequest, PhoneEnrollmentInfo,
75    PhoneSignInInfo, PhoneVerificationInfo, StartPasskeyMfaEnrollmentRequest,
76    StartPasskeyMfaSignInRequest, StartPhoneMfaEnrollmentRequest, StartPhoneMfaSignInRequest,
77    StartTotpMfaEnrollmentRequest, TotpSignInVerificationInfo, TotpVerificationInfo,
78    WebAuthnVerificationInfo, WithdrawMfaRequest,
79};
80use phone::{
81    link_with_phone_number as api_link_with_phone_number, send_phone_verification_code,
82    sign_in_with_phone_number as api_sign_in_with_phone_number, verify_phone_number_for_existing,
83    PhoneSignInResponse, SendPhoneVerificationCodeRequest, SignInWithPhoneNumberRequest,
84};
85use serde_json::Value;
86
87const DEFAULT_OAUTH_REQUEST_URI: &str = "http://localhost";
88const DEFAULT_IDENTITY_TOOLKIT_ENDPOINT: &str = "https://identitytoolkit.googleapis.com/v1";
89const CLIENT_TYPE_WEB: &str = "CLIENT_TYPE_WEB";
90const RECAPTCHA_ENTERPRISE: &str = "RECAPTCHA_ENTERPRISE";
91
92struct SignInResponsePayload<'a> {
93    local_id: &'a str,
94    email: Option<&'a str>,
95    phone_number: Option<&'a str>,
96    id_token: &'a str,
97    refresh_token: &'a str,
98    expires_in: Option<&'a str>,
99    provider_id: Option<&'a str>,
100    operation: &'a str,
101    anonymous: bool,
102}
103
104#[derive(Clone)]
105enum PhoneFinalization {
106    SignIn,
107    Link { id_token: String },
108    Reauth { id_token: String },
109}
110
111struct PhoneMfaEnrollmentFinalization {
112    id_token: String,
113    session_info: String,
114    display_name: Option<String>,
115}
116
117pub struct Auth {
118    app: FirebaseApp,
119    config: Mutex<AuthConfig>,
120    current_user: Mutex<Option<Arc<User>>>,
121    listeners: AuthStateListeners,
122    rest_client: Client,
123    token_refresh_tolerance: Duration,
124    persistence: Arc<dyn AuthPersistence + Send + Sync>,
125    persisted_state_cache: Mutex<Option<PersistedAuthState>>,
126    persistence_subscription: Mutex<Option<PersistenceSubscription>>,
127    popup_handler: Mutex<Option<Arc<dyn OAuthPopupHandler>>>,
128    redirect_handler: Mutex<Option<Arc<dyn OAuthRedirectHandler>>>,
129    redirect_persistence: Mutex<Arc<dyn RedirectPersistence>>,
130    oauth_request_uri: Mutex<String>,
131    identity_toolkit_endpoint: Mutex<String>,
132    secure_token_endpoint: Mutex<String>,
133    refresh_cancel: Mutex<Option<Arc<AtomicBool>>>,
134    self_ref: Mutex<Weak<Auth>>,
135}
136
137#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
138#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
139impl AsyncTokenProvider for Arc<Auth> {
140    async fn get_token(&self, force_refresh: bool) -> Result<Option<String>, TokenError> {
141        self.get_token(force_refresh)
142            .await
143            .map_err(TokenError::from_error)
144    }
145}
146
147impl Auth {
148    /// Returns a multi-factor helper tied to this auth instance.
149    pub fn multi_factor(self: &Arc<Self>) -> MultiFactorUser {
150        MultiFactorUser::new(self.clone())
151    }
152
153    /// Creates a builder for configuring an `Auth` instance before construction.
154    pub fn builder(app: FirebaseApp) -> AuthBuilder {
155        AuthBuilder::new(app)
156    }
157
158    /// Constructs an `Auth` instance using in-memory persistence.
159    pub fn new(app: FirebaseApp) -> AuthResult<Self> {
160        #[cfg(all(
161            feature = "wasm-web",
162            feature = "experimental-indexed-db",
163            target_arch = "wasm32"
164        ))]
165        let persistence: Arc<dyn AuthPersistence + Send + Sync> =
166            Arc::new(IndexedDbPersistence::new());
167
168        #[cfg(all(
169            feature = "wasm-web",
170            not(feature = "experimental-indexed-db"),
171            target_arch = "wasm32"
172        ))]
173        let persistence: Arc<dyn AuthPersistence + Send + Sync> =
174            Arc::new(InMemoryPersistence::default());
175
176        #[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
177        let persistence: Arc<dyn AuthPersistence + Send + Sync> =
178            Arc::new(InMemoryPersistence::default());
179        Self::new_with_persistence(app, persistence)
180    }
181
182    /// Constructs an `Auth` instance with a caller-provided persistence backend.
183    pub fn new_with_persistence(
184        app: FirebaseApp,
185        persistence: Arc<dyn AuthPersistence + Send + Sync>,
186    ) -> AuthResult<Self> {
187        let api_key = app
188            .options()
189            .api_key
190            .clone()
191            .ok_or_else(|| AuthError::InvalidCredential("Missing API key".into()))?;
192
193        let config = AuthConfig {
194            api_key: Some(api_key),
195            identity_toolkit_endpoint: Some(DEFAULT_IDENTITY_TOOLKIT_ENDPOINT.to_string()),
196            secure_token_endpoint: Some(token::DEFAULT_SECURE_TOKEN_ENDPOINT.to_string()),
197        };
198
199        Ok(Self {
200            app,
201            config: Mutex::new(config),
202            current_user: Mutex::new(None),
203            listeners: AuthStateListeners::default(),
204            rest_client: Client::new(),
205            token_refresh_tolerance: Duration::from_secs(5 * 60),
206            persistence,
207            persisted_state_cache: Mutex::new(None),
208            persistence_subscription: Mutex::new(None),
209            popup_handler: Mutex::new(None),
210            redirect_handler: Mutex::new(None),
211            redirect_persistence: Mutex::new(InMemoryRedirectPersistence::shared()),
212            oauth_request_uri: Mutex::new(DEFAULT_OAUTH_REQUEST_URI.to_string()),
213            identity_toolkit_endpoint: Mutex::new(DEFAULT_IDENTITY_TOOLKIT_ENDPOINT.to_string()),
214            secure_token_endpoint: Mutex::new(token::DEFAULT_SECURE_TOKEN_ENDPOINT.to_string()),
215            refresh_cancel: Mutex::new(None),
216            self_ref: Mutex::new(Weak::new()),
217        })
218    }
219
220    /// Finishes initialization by restoring persisted state and wiring listeners.
221    pub fn initialize(self: &Arc<Self>) -> AuthResult<()> {
222        *self.self_ref.lock().unwrap() = Arc::downgrade(self);
223        self.restore_from_persistence()?;
224        self.install_persistence_subscription()?;
225        Ok(())
226    }
227
228    /// Returns the `FirebaseApp` associated with this Auth instance.
229    pub fn app(&self) -> &FirebaseApp {
230        &self.app
231    }
232
233    /// Returns the currently signed-in user, if any.
234    pub fn current_user(&self) -> Option<Arc<User>> {
235        self.current_user.lock().unwrap().clone()
236    }
237
238    /// Signs out the current user and clears persisted credentials.
239    pub fn sign_out(&self) {
240        self.clear_local_user_state();
241        if let Err(err) = self.set_persisted_state(None) {
242            eprintln!("Failed to clear persisted auth state: {err}");
243        }
244    }
245
246    /// Returns the email/password auth provider helper.
247    pub fn email_auth_provider(&self) -> EmailAuthProvider {
248        EmailAuthProvider
249    }
250
251    /// Signs a user in using the email/password REST endpoint.
252    pub async fn sign_in_with_email_and_password(
253        &self,
254        email: &str,
255        password: &str,
256    ) -> AuthResult<UserCredential> {
257        let api_key = self.api_key()?;
258
259        let request = SignInWithPasswordRequest {
260            email: email.to_owned(),
261            password: password.to_owned(),
262            return_secure_token: true,
263        };
264
265        let response: SignInWithPasswordResponse = self
266            .execute_request("accounts:signInWithPassword", &api_key, &request)
267            .await?;
268
269        if let Some(pending) = response.mfa_pending_credential.clone() {
270            let mut context = MultiFactorSignInContext::default();
271            context.local_id = Some(response.local_id.clone());
272            context.email = Some(response.email.clone());
273            context.provider_id = Some(EmailAuthProvider::PROVIDER_ID.to_string());
274            context.anonymous = false;
275
276            return Err(self.build_multi_factor_error(
277                MultiFactorOperation::SignIn,
278                pending,
279                response.mfa_info.clone(),
280                context,
281                None,
282            ));
283        }
284
285        let payload = SignInResponsePayload {
286            local_id: &response.local_id,
287            email: Some(&response.email),
288            phone_number: None,
289            id_token: response
290                .id_token
291                .as_deref()
292                .ok_or_else(|| AuthError::InvalidCredential("Missing idToken".into()))?,
293            refresh_token: response
294                .refresh_token
295                .as_deref()
296                .ok_or_else(|| AuthError::InvalidCredential("Missing refreshToken".into()))?,
297            expires_in: response.expires_in.as_deref(),
298            provider_id: Some(EmailAuthProvider::PROVIDER_ID),
299            operation: "signIn",
300            anonymous: false,
301        };
302
303        self.finalize_sign_in(payload)
304    }
305
306    /// Creates a new user using email/password credentials.
307    pub async fn create_user_with_email_and_password(
308        &self,
309        email: &str,
310        password: &str,
311    ) -> AuthResult<UserCredential> {
312        let api_key = self.api_key()?;
313        let mut request = SignUpRequest::default();
314        request.email = Some(email.to_owned());
315        request.password = Some(password.to_owned());
316        request.return_secure_token = Some(true);
317
318        let response: SignUpResponse = self
319            .execute_request("accounts:signUp", &api_key, &request)
320            .await?;
321
322        let local_id = response
323            .local_id
324            .as_deref()
325            .ok_or_else(|| AuthError::InvalidCredential("Missing localId".into()))?;
326        let id_token = response
327            .id_token
328            .as_deref()
329            .ok_or_else(|| AuthError::InvalidCredential("Missing idToken".into()))?;
330        let refresh_token = response
331            .refresh_token
332            .as_deref()
333            .ok_or_else(|| AuthError::InvalidCredential("Missing refreshToken".into()))?;
334        let expires_in = response.expires_in.as_deref();
335        let response_email = response.email.as_deref().unwrap_or(email);
336
337        let payload = SignInResponsePayload {
338            local_id,
339            email: Some(response_email),
340            phone_number: None,
341            id_token,
342            refresh_token,
343            expires_in,
344            provider_id: Some(EmailAuthProvider::PROVIDER_ID),
345            operation: "signUp",
346            anonymous: false,
347        };
348
349        self.finalize_sign_in(payload)
350    }
351
352    /// Exchanges a custom authentication token for Firebase credentials.
353    ///
354    /// # Examples
355    /// ```rust,no_run
356    /// # use firebase_rs_sdk::doctest_support::get_mock_auth;
357    /// # use firebase_rs_sdk::auth::AuthError;
358    /// # async fn run() -> Result<(), AuthError> {
359    /// # let auth = get_mock_auth(None).await;
360    /// let credential = auth.sign_in_with_custom_token("CUSTOM-TOKEN").await?;
361    /// println!("Signed in as {}", credential.user.uid());
362    /// # Ok(()) }
363    /// ```
364    pub async fn sign_in_with_custom_token(&self, token: &str) -> AuthResult<UserCredential> {
365        let api_key = self.api_key()?;
366        let request = SignInWithCustomTokenRequest {
367            token: token.to_owned(),
368            return_secure_token: true,
369        };
370
371        let response: SignInWithCustomTokenResponse = self
372            .execute_request("accounts:signInWithCustomToken", &api_key, &request)
373            .await?;
374
375        if let Some(pending) = response.mfa_pending_credential.clone() {
376            let mut context = MultiFactorSignInContext::default();
377            context.local_id = response.local_id.clone();
378            context.email = response.email.clone();
379            context.provider_id = Some("custom".to_string());
380            context.anonymous = false;
381
382            return Err(self.build_multi_factor_error(
383                MultiFactorOperation::SignIn,
384                pending,
385                response.mfa_info.clone(),
386                context,
387                None,
388            ));
389        }
390
391        let local_id = response
392            .local_id
393            .as_deref()
394            .ok_or_else(|| AuthError::InvalidCredential("Missing localId".into()))?;
395        let id_token = response
396            .id_token
397            .as_deref()
398            .ok_or_else(|| AuthError::InvalidCredential("Missing idToken".into()))?;
399        let refresh_token = response
400            .refresh_token
401            .as_deref()
402            .ok_or_else(|| AuthError::InvalidCredential("Missing refreshToken".into()))?;
403        let expires_in = response.expires_in.as_deref();
404        let email = response.email.as_deref();
405        let operation = if response.is_new_user.unwrap_or(false) {
406            "signUp"
407        } else {
408            "signIn"
409        };
410
411        let payload = SignInResponsePayload {
412            local_id,
413            email,
414            phone_number: None,
415            id_token,
416            refresh_token,
417            expires_in,
418            provider_id: Some("custom"),
419            operation,
420            anonymous: false,
421        };
422
423        self.finalize_sign_in(payload)
424    }
425
426    /// Signs the user in anonymously, creating an anonymous user if needed.
427    ///
428    /// # Examples
429    /// ```rust,no_run
430    /// # use firebase_rs_sdk::doctest_support::get_mock_auth;
431    /// # use firebase_rs_sdk::auth::AuthError;
432    /// # async fn run() -> Result<(), AuthError> {
433    /// # let auth = get_mock_auth(None).await;
434    /// let anon = auth.sign_in_anonymously().await?;
435    /// assert!(anon.user.is_anonymous());
436    /// # Ok(()) }
437    /// ```
438    pub async fn sign_in_anonymously(&self) -> AuthResult<UserCredential> {
439        if let Some(user) = self.current_user() {
440            if user.is_anonymous() {
441                return Ok(UserCredential {
442                    user,
443                    provider_id: Some("anonymous".to_string()),
444                    operation_type: Some("signIn".to_string()),
445                });
446            }
447        }
448
449        let api_key = self.api_key()?;
450        let mut request = SignUpRequest::default();
451        request.return_secure_token = Some(true);
452
453        let response: SignUpResponse = self
454            .execute_request("accounts:signUp", &api_key, &request)
455            .await?;
456
457        let local_id = response
458            .local_id
459            .as_deref()
460            .ok_or_else(|| AuthError::InvalidCredential("Missing localId".into()))?;
461        let id_token = response
462            .id_token
463            .as_deref()
464            .ok_or_else(|| AuthError::InvalidCredential("Missing idToken".into()))?;
465        let refresh_token = response
466            .refresh_token
467            .as_deref()
468            .ok_or_else(|| AuthError::InvalidCredential("Missing refreshToken".into()))?;
469        let expires_in = response.expires_in.as_deref();
470
471        let payload = SignInResponsePayload {
472            local_id,
473            email: None,
474            phone_number: None,
475            id_token,
476            refresh_token,
477            expires_in,
478            provider_id: Some("anonymous"),
479            operation: "signIn",
480            anonymous: true,
481        };
482
483        self.finalize_sign_in(payload)
484    }
485
486    /// Starts a phone number sign-in flow, returning a confirmation handle.
487    pub async fn sign_in_with_phone_number(
488        self: &Arc<Self>,
489        phone_number: &str,
490        verifier: Arc<dyn ApplicationVerifier>,
491    ) -> AuthResult<ConfirmationResult> {
492        self.start_phone_flow(phone_number, verifier, PhoneFinalization::SignIn)
493            .await
494    }
495
496    /// Sends an SMS verification code and returns the verification identifier.
497    ///
498    /// # Examples
499    /// ```rust,no_run
500    /// # use std::sync::Arc;
501    /// # use firebase_rs_sdk::doctest_support::{get_mock_auth, auth::MockVerifier};
502    /// # use firebase_rs_sdk::auth::AuthError;
503    /// # async fn run() -> Result<(), AuthError> {
504    /// # let auth = get_mock_auth(None).await;
505    /// # let verifier = Arc::new(MockVerifier{token: "verifier-token", kind: "verifier-kind"});
506    /// let verification_id = auth
507    ///     .send_phone_verification_code("+15551234567", verifier)
508    ///     .await?;
509    /// println!("Sent verification code, id: {}", verification_id);
510    /// # Ok(()) }
511    /// ```
512    pub async fn send_phone_verification_code(
513        &self,
514        phone_number: &str,
515        verifier: Arc<dyn ApplicationVerifier>,
516    ) -> AuthResult<String> {
517        self.send_phone_verification(phone_number, verifier).await
518    }
519
520    /// Links the currently signed-in account with the provided phone number.
521    pub async fn link_with_phone_number(
522        self: &Arc<Self>,
523        phone_number: &str,
524        verifier: Arc<dyn ApplicationVerifier>,
525    ) -> AuthResult<ConfirmationResult> {
526        let user = self.require_current_user()?;
527        let id_token = user.get_id_token(false)?;
528        self.start_phone_flow(phone_number, verifier, PhoneFinalization::Link { id_token })
529            .await
530    }
531
532    /// Reauthenticates the current user via an SMS verification code.
533    pub async fn reauthenticate_with_phone_number(
534        self: &Arc<Self>,
535        phone_number: &str,
536        verifier: Arc<dyn ApplicationVerifier>,
537    ) -> AuthResult<ConfirmationResult> {
538        let user = self.require_current_user()?;
539        let id_token = user.get_id_token(false)?;
540        self.start_phone_flow(
541            phone_number,
542            verifier,
543            PhoneFinalization::Reauth { id_token },
544        )
545        .await
546    }
547
548    /// Signs in using a credential produced by [`PhoneAuthProvider::credential`].
549    ///
550    /// # Examples
551    /// ```rust,no_run
552    /// # use firebase_rs_sdk::doctest_support::get_mock_auth;
553    /// # use firebase_rs_sdk::auth::AuthError;
554    /// # async fn run() -> Result<(), AuthError> {
555    /// # let auth = get_mock_auth(None).await;
556    /// let verification_id = "verification-id-from-sms";
557    /// let sms_code = "123456";
558    /// let credential = firebase_rs_sdk::auth::PhoneAuthProvider::credential(verification_id, sms_code);
559    /// let user = auth.sign_in_with_phone_credential(credential).await?;
560    /// # Ok(()) }
561    /// ```
562    pub async fn sign_in_with_phone_credential(
563        self: &Arc<Self>,
564        credential: PhoneAuthCredential,
565    ) -> AuthResult<UserCredential> {
566        self.finalize_phone_credential(credential, PhoneFinalization::SignIn)
567            .await
568    }
569
570    /// Links the current user with the provided phone credential.
571    pub async fn link_with_phone_credential(
572        self: &Arc<Self>,
573        credential: PhoneAuthCredential,
574    ) -> AuthResult<UserCredential> {
575        let user = self.require_current_user()?;
576        let id_token = user.get_id_token(false)?;
577        self.finalize_phone_credential(credential, PhoneFinalization::Link { id_token })
578            .await
579    }
580
581    /// Reauthenticates the current user with an SMS credential.
582    ///
583    /// # Examples
584    /// ```rust,no_run
585    /// # use firebase_rs_sdk::doctest_support::get_mock_auth;
586    /// # use firebase_rs_sdk::auth::AuthError;
587    /// # async fn run() -> Result<(), AuthError> {
588    /// # let auth = get_mock_auth(None).await;
589    /// let verification_id = "verification-id-from-sms";
590    /// let sms_code = "123456";
591    /// let credential = firebase_rs_sdk::auth::PhoneAuthProvider::credential(verification_id, sms_code);
592    /// let user = auth.reauthenticate_with_phone_credential(credential).await?;
593    /// # Ok(()) }
594    /// ```
595    pub async fn reauthenticate_with_phone_credential(
596        self: &Arc<Self>,
597        credential: PhoneAuthCredential,
598    ) -> AuthResult<Arc<User>> {
599        let user = self.require_current_user()?;
600        let id_token = user.get_id_token(false)?;
601        let result = self
602            .finalize_phone_credential(credential, PhoneFinalization::Reauth { id_token })
603            .await?;
604        Ok(result.user)
605    }
606
607    /// Registers an observer that is invoked whenever auth state changes.
608    pub fn on_auth_state_changed(
609        &self,
610        observer: PartialObserver<Arc<User>>,
611    ) -> impl FnOnce() + Send + 'static {
612        if let Some(user) = self.current_user() {
613            if let Some(next) = observer.next.clone() {
614                next(&user);
615            }
616        }
617
618        self.listeners.add_observer(observer);
619        || {}
620    }
621
622    async fn execute_request<TRequest, TResponse>(
623        &self,
624        path: &str,
625        api_key: &str,
626        request: &TRequest,
627    ) -> AuthResult<TResponse>
628    where
629        TRequest: Serialize,
630        TResponse: serde::de::DeserializeOwned + 'static,
631    {
632        let client = self.rest_client.clone();
633        let url = self.endpoint_url(path, api_key)?;
634        let body =
635            serde_json::to_vec(request).map_err(|err| AuthError::Network(err.to_string()))?;
636
637        let response = client
638            .post(url)
639            .header("content-type", "application/json")
640            .body(body)
641            .send()
642            .await
643            .map_err(|err| AuthError::Network(err.to_string()))?;
644
645        if !response.status().is_success() {
646            let message = response
647                .text()
648                .await
649                .unwrap_or_else(|_| "Unknown error".to_string());
650            return Err(AuthError::Network(message));
651        }
652
653        response
654            .json()
655            .await
656            .map_err(|err| AuthError::Network(err.to_string()))
657    }
658
659    async fn start_phone_flow(
660        self: &Arc<Self>,
661        phone_number: &str,
662        verifier: Arc<dyn ApplicationVerifier>,
663        flow: PhoneFinalization,
664    ) -> AuthResult<ConfirmationResult> {
665        let verification_id = self
666            .send_phone_verification(phone_number, Arc::clone(&verifier))
667            .await?;
668        let session_info = Arc::new(verification_id.clone());
669        let auth = Arc::clone(self);
670        let flow_holder = Arc::new(flow);
671
672        Ok(ConfirmationResult::new(
673            verification_id,
674            move |code: &str| {
675                let auth = Arc::clone(&auth);
676                let session = Arc::clone(&session_info);
677                let flow = Arc::clone(&flow_holder);
678                let code = code.to_owned();
679                async move {
680                    Auth::finalize_phone_confirmation(
681                        auth,
682                        (*session).clone(),
683                        code,
684                        (*flow).clone(),
685                    )
686                    .await
687                }
688            },
689        ))
690    }
691
692    pub(crate) async fn send_phone_verification(
693        &self,
694        phone_number: &str,
695        verifier: Arc<dyn ApplicationVerifier>,
696    ) -> AuthResult<String> {
697        let api_key = self.api_key()?;
698        let endpoint = self.identity_toolkit_endpoint();
699        let request = self.build_phone_verification_request(phone_number, verifier)?;
700        let response =
701            send_phone_verification_code(&self.rest_client, &endpoint, &api_key, &request).await?;
702        Ok(response.session_info)
703    }
704
705    fn build_phone_verification_request(
706        &self,
707        phone_number: &str,
708        verifier: Arc<dyn ApplicationVerifier>,
709    ) -> AuthResult<SendPhoneVerificationCodeRequest> {
710        let token = verifier.verify()?;
711        let verifier_type = verifier.verifier_type().to_lowercase();
712        let mut request = SendPhoneVerificationCodeRequest {
713            phone_number: phone_number.to_string(),
714            ..Default::default()
715        };
716
717        match verifier_type.as_str() {
718            "recaptcha" | "recaptcha-v2" => {
719                request.recaptcha_token = Some(token);
720            }
721            "recaptcha-enterprise" => {
722                request.captcha_response = Some(token);
723                request.client_type = Some(CLIENT_TYPE_WEB.to_string());
724                request.recaptcha_version = Some(RECAPTCHA_ENTERPRISE.to_string());
725            }
726            other => {
727                request.captcha_response = Some(token);
728                request.client_type = Some(other.to_string());
729            }
730        }
731
732        if request.client_type.is_none() {
733            request.client_type = Some(CLIENT_TYPE_WEB.to_string());
734        }
735
736        Ok(request)
737    }
738
739    fn build_phone_enrollment_info(
740        &self,
741        phone_number: &str,
742        verifier: Arc<dyn ApplicationVerifier>,
743    ) -> AuthResult<PhoneEnrollmentInfo> {
744        let token = verifier.verify()?;
745        let verifier_type = verifier.verifier_type().to_lowercase();
746        let mut info = PhoneEnrollmentInfo {
747            phone_number: phone_number.to_string(),
748            recaptcha_token: None,
749            captcha_response: None,
750            client_type: Some(CLIENT_TYPE_WEB.to_string()),
751            recaptcha_version: None,
752        };
753
754        match verifier_type.as_str() {
755            "recaptcha" | "recaptcha-v2" => {
756                info.recaptcha_token = Some(token);
757            }
758            "recaptcha-enterprise" => {
759                info.captcha_response = Some(token);
760                info.recaptcha_version = Some(RECAPTCHA_ENTERPRISE.to_string());
761            }
762            other => {
763                info.captcha_response = Some(token);
764                info.client_type = Some(other.to_string());
765            }
766        }
767
768        if info.client_type.is_none() {
769            info.client_type = Some(CLIENT_TYPE_WEB.to_string());
770        }
771
772        Ok(info)
773    }
774
775    fn build_phone_sign_in_info(
776        &self,
777        verifier: Arc<dyn ApplicationVerifier>,
778    ) -> AuthResult<PhoneSignInInfo> {
779        let token = verifier.verify()?;
780        let verifier_type = verifier.verifier_type().to_lowercase();
781        let mut info = PhoneSignInInfo {
782            recaptcha_token: None,
783            captcha_response: None,
784            client_type: Some(CLIENT_TYPE_WEB.to_string()),
785            recaptcha_version: None,
786        };
787
788        match verifier_type.as_str() {
789            "recaptcha" | "recaptcha-v2" => {
790                info.recaptcha_token = Some(token);
791            }
792            "recaptcha-enterprise" => {
793                info.captcha_response = Some(token);
794                info.recaptcha_version = Some(RECAPTCHA_ENTERPRISE.to_string());
795            }
796            other => {
797                info.captcha_response = Some(token);
798                info.client_type = Some(other.to_string());
799            }
800        }
801
802        if info.client_type.is_none() {
803            info.client_type = Some(CLIENT_TYPE_WEB.to_string());
804        }
805
806        Ok(info)
807    }
808
809    pub(crate) async fn start_phone_mfa_enrollment(
810        self: &Arc<Self>,
811        phone_number: &str,
812        verifier: Arc<dyn ApplicationVerifier>,
813        display_name: Option<&str>,
814    ) -> AuthResult<ConfirmationResult> {
815        let user = self.require_current_user()?;
816        let id_token = user.get_id_token(false)?;
817        let api_key = self.api_key()?;
818        let endpoint = self.identity_toolkit_endpoint();
819        let enrollment_info = self.build_phone_enrollment_info(phone_number, verifier)?;
820        let request = StartPhoneMfaEnrollmentRequest {
821            id_token: id_token.clone(),
822            phone_enrollment_info: enrollment_info,
823            tenant_id: None,
824        };
825
826        let response =
827            start_phone_mfa_enrollment(&self.rest_client, &endpoint, &api_key, &request).await?;
828
829        let session_info = response.phone_session_info.session_info;
830        let auth = Arc::clone(self);
831        let display_name = display_name.map(|value| value.to_string());
832        let id_token_for_flow = id_token.clone();
833        Ok(ConfirmationResult::new(
834            session_info.clone(),
835            move |code: &str| {
836                let auth = Arc::clone(&auth);
837                let code = code.to_string();
838                let session = session_info.clone();
839                let display = display_name.clone();
840                let id_token_value = id_token_for_flow.clone();
841                async move {
842                    auth.complete_phone_mfa_enrollment(
843                        PhoneMfaEnrollmentFinalization {
844                            id_token: id_token_value,
845                            session_info: session,
846                            display_name: display,
847                        },
848                        code,
849                    )
850                    .await
851                }
852            },
853        ))
854    }
855
856    pub(crate) async fn start_totp_mfa_enrollment(
857        self: &Arc<Self>,
858        session: &MultiFactorSession,
859    ) -> AuthResult<TotpSecret> {
860        if session.session_type() != MultiFactorSessionType::Enrollment {
861            return Err(AuthError::InvalidCredential(
862                "TOTP enrollment requires an enrollment session".into(),
863            ));
864        }
865
866        let id_token = session.id_token().ok_or_else(|| {
867            AuthError::InvalidCredential("Missing ID token for TOTP enrollment".into())
868        })?;
869        let api_key = self.api_key()?;
870        let endpoint = self.identity_toolkit_endpoint();
871        let request = StartTotpMfaEnrollmentRequest {
872            id_token: id_token.to_string(),
873            totp_enrollment_info: serde_json::json!({}),
874            tenant_id: None,
875        };
876
877        let response =
878            start_totp_mfa_enrollment(&self.rest_client, &endpoint, &api_key, &request).await?;
879        let info = response.totp_session_info;
880        let deadline = if info.finalize_enrollment_time <= 0 {
881            UNIX_EPOCH
882        } else {
883            UNIX_EPOCH
884                .checked_add(Duration::from_millis(info.finalize_enrollment_time as u64))
885                .unwrap_or(UNIX_EPOCH)
886        };
887
888        Ok(TotpSecret::new(
889            self,
890            info.shared_secret_key,
891            info.hashing_algorithm,
892            info.verification_code_length,
893            info.period_sec,
894            deadline,
895            info.session_info,
896        ))
897    }
898
899    pub(crate) async fn start_passkey_mfa_enrollment(
900        self: &Arc<Self>,
901        session: &MultiFactorSession,
902    ) -> AuthResult<WebAuthnEnrollmentChallenge> {
903        if session.session_type() != MultiFactorSessionType::Enrollment {
904            return Err(AuthError::InvalidCredential(
905                "Passkey enrollment requires an enrollment session".into(),
906            ));
907        }
908
909        let id_token = session.id_token().ok_or_else(|| {
910            AuthError::InvalidCredential("Missing ID token for enrollment".into())
911        })?;
912        let api_key = self.api_key()?;
913        let endpoint = self.identity_toolkit_endpoint();
914        let request = StartPasskeyMfaEnrollmentRequest {
915            id_token: id_token.to_string(),
916            webauthn_enrollment_info: serde_json::json!({}),
917            display_name: None,
918            tenant_id: None,
919        };
920
921        let response =
922            start_passkey_mfa_enrollment(&self.rest_client, &endpoint, &api_key, &request).await?;
923        let challenge = WebAuthnEnrollmentChallenge::from_value(response.webauthn_enrollment_info)?;
924        Ok(challenge)
925    }
926
927    pub(crate) async fn start_phone_multi_factor_sign_in(
928        self: &Arc<Self>,
929        pending_credential: &str,
930        enrollment_id: &str,
931        verifier: Arc<dyn ApplicationVerifier>,
932    ) -> AuthResult<String> {
933        let api_key = self.api_key()?;
934        let endpoint = self.identity_toolkit_endpoint();
935        let sign_in_info = self.build_phone_sign_in_info(verifier)?;
936        let request = StartPhoneMfaSignInRequest {
937            mfa_pending_credential: pending_credential.to_string(),
938            mfa_enrollment_id: enrollment_id.to_string(),
939            phone_sign_in_info: sign_in_info,
940            tenant_id: None,
941        };
942
943        let response =
944            start_phone_mfa_sign_in(&self.rest_client, &endpoint, &api_key, &request).await?;
945        Ok(response.phone_response_info.session_info)
946    }
947
948    pub(crate) async fn start_passkey_multi_factor_sign_in(
949        self: &Arc<Self>,
950        pending_credential: &str,
951        enrollment_id: &str,
952    ) -> AuthResult<WebAuthnSignInChallenge> {
953        let api_key = self.api_key()?;
954        let endpoint = self.identity_toolkit_endpoint();
955        let request = StartPasskeyMfaSignInRequest {
956            mfa_pending_credential: pending_credential.to_string(),
957            mfa_enrollment_id: enrollment_id.to_string(),
958            webauthn_sign_in_info: None,
959            tenant_id: None,
960        };
961
962        let response =
963            start_passkey_mfa_sign_in(&self.rest_client, &endpoint, &api_key, &request).await?;
964        let challenge = WebAuthnSignInChallenge::from_value(response.webauthn_sign_in_info)?;
965        Ok(challenge)
966    }
967
968    async fn complete_phone_mfa_enrollment(
969        self: Arc<Self>,
970        flow: PhoneMfaEnrollmentFinalization,
971        verification_code: String,
972    ) -> AuthResult<UserCredential> {
973        let api_key = self.api_key()?;
974        let endpoint = self.identity_toolkit_endpoint();
975        let request = FinalizePhoneMfaEnrollmentRequest {
976            id_token: flow.id_token.clone(),
977            phone_verification_info: PhoneVerificationInfo {
978                session_info: flow.session_info.clone(),
979                code: verification_code,
980            },
981            display_name: flow.display_name.clone(),
982            tenant_id: None,
983        };
984
985        let response =
986            finalize_phone_mfa_enrollment(&self.rest_client, &endpoint, &api_key, &request).await?;
987
988        self.update_current_user_tokens(
989            response.id_token.clone(),
990            response.refresh_token.clone(),
991            None,
992        )?;
993        let user = self.refresh_current_user_profile().await?;
994
995        Ok(UserCredential {
996            user,
997            provider_id: Some(PHONE_PROVIDER_ID.to_string()),
998            operation_type: Some("enroll".to_string()),
999        })
1000    }
1001
1002    pub(crate) async fn complete_totp_mfa_enrollment(
1003        self: &Arc<Self>,
1004        id_token: &str,
1005        secret: &TotpSecret,
1006        otp: &str,
1007        display_name: Option<&str>,
1008    ) -> AuthResult<UserCredential> {
1009        let api_key = self.api_key()?;
1010        let endpoint = self.identity_toolkit_endpoint();
1011        let request = FinalizeTotpMfaEnrollmentRequest {
1012            id_token: id_token.to_string(),
1013            totp_verification_info: TotpVerificationInfo {
1014                session_info: secret.session_info().to_string(),
1015                verification_code: otp.to_string(),
1016            },
1017            display_name: display_name.map(|value| value.to_string()),
1018            tenant_id: None,
1019        };
1020
1021        let response =
1022            finalize_totp_mfa_enrollment(&self.rest_client, &endpoint, &api_key, &request).await?;
1023
1024        self.update_current_user_tokens(
1025            response.id_token.clone(),
1026            response.refresh_token.clone(),
1027            None,
1028        )?;
1029        let user = self.refresh_current_user_profile().await?;
1030
1031        Ok(UserCredential {
1032            user,
1033            provider_id: Some("totp".to_string()),
1034            operation_type: Some("enroll".to_string()),
1035        })
1036    }
1037
1038    pub(crate) async fn complete_passkey_mfa_enrollment(
1039        self: &Arc<Self>,
1040        session: &MultiFactorSession,
1041        attestation: WebAuthnAttestationResponse,
1042        display_name: Option<&str>,
1043    ) -> AuthResult<UserCredential> {
1044        if session.session_type() != MultiFactorSessionType::Enrollment {
1045            return Err(AuthError::InvalidCredential(
1046                "Passkey enrollment requires an enrollment session".into(),
1047            ));
1048        }
1049
1050        let id_token = session.id_token().ok_or_else(|| {
1051            AuthError::InvalidCredential("Missing ID token for enrollment".into())
1052        })?;
1053        let api_key = self.api_key()?;
1054        let endpoint = self.identity_toolkit_endpoint();
1055        let request = FinalizePasskeyMfaEnrollmentRequest {
1056            id_token: id_token.to_string(),
1057            webauthn_verification_info: WebAuthnVerificationInfo {
1058                payload: attestation.into_raw(),
1059            },
1060            display_name: display_name.map(|value| value.to_string()),
1061            tenant_id: None,
1062        };
1063
1064        let response =
1065            finalize_passkey_mfa_enrollment(&self.rest_client, &endpoint, &api_key, &request)
1066                .await?;
1067
1068        self.update_current_user_tokens(
1069            response.id_token.clone(),
1070            response.refresh_token.clone(),
1071            None,
1072        )?;
1073        let user = self.refresh_current_user_profile().await?;
1074
1075        Ok(UserCredential {
1076            user,
1077            provider_id: Some(WEBAUTHN_FACTOR_ID.to_string()),
1078            operation_type: Some("enroll".to_string()),
1079        })
1080    }
1081
1082    pub(crate) async fn finalize_phone_multi_factor_sign_in(
1083        self: &Arc<Self>,
1084        pending_credential: &str,
1085        session_info: &str,
1086        verification_code: &str,
1087        context: Arc<MultiFactorSignInContext>,
1088        operation: MultiFactorOperation,
1089    ) -> AuthResult<UserCredential> {
1090        let api_key = self.api_key()?;
1091        let endpoint = self.identity_toolkit_endpoint();
1092        let request = FinalizePhoneMfaSignInRequest {
1093            mfa_pending_credential: pending_credential.to_string(),
1094            phone_verification_info: PhoneVerificationInfo {
1095                session_info: session_info.to_string(),
1096                code: verification_code.to_string(),
1097            },
1098            tenant_id: None,
1099        };
1100
1101        let response =
1102            finalize_phone_mfa_sign_in(&self.rest_client, &endpoint, &api_key, &request).await?;
1103
1104        let context = context.as_ref();
1105        let local_id = context.local_id.as_deref().ok_or_else(|| {
1106            AuthError::InvalidCredential("Missing localId for multi-factor sign-in".into())
1107        })?;
1108
1109        let payload = SignInResponsePayload {
1110            local_id,
1111            email: context.email.as_deref(),
1112            phone_number: context.phone_number.as_deref(),
1113            id_token: response.id_token.as_str(),
1114            refresh_token: response.refresh_token.as_str(),
1115            expires_in: None,
1116            provider_id: context.provider_id.as_deref(),
1117            operation: context.operation_label(operation),
1118            anonymous: context.anonymous,
1119        };
1120
1121        self.finalize_sign_in(payload)
1122    }
1123
1124    pub(crate) async fn finalize_totp_multi_factor_sign_in(
1125        self: &Arc<Self>,
1126        pending_credential: &str,
1127        enrollment_id: &str,
1128        otp: &str,
1129        context: Arc<MultiFactorSignInContext>,
1130        operation: MultiFactorOperation,
1131    ) -> AuthResult<UserCredential> {
1132        let api_key = self.api_key()?;
1133        let endpoint = self.identity_toolkit_endpoint();
1134        let request = FinalizeTotpMfaSignInRequest {
1135            mfa_pending_credential: pending_credential.to_string(),
1136            mfa_enrollment_id: enrollment_id.to_string(),
1137            totp_verification_info: TotpSignInVerificationInfo {
1138                verification_code: otp.to_string(),
1139            },
1140            tenant_id: None,
1141        };
1142
1143        let response =
1144            finalize_totp_mfa_sign_in(&self.rest_client, &endpoint, &api_key, &request).await?;
1145
1146        let context = context.as_ref();
1147        let local_id = context.local_id.as_deref().ok_or_else(|| {
1148            AuthError::InvalidCredential("Missing localId for multi-factor sign-in".into())
1149        })?;
1150
1151        let payload = SignInResponsePayload {
1152            local_id,
1153            email: context.email.as_deref(),
1154            phone_number: context.phone_number.as_deref(),
1155            id_token: response.id_token.as_str(),
1156            refresh_token: response.refresh_token.as_str(),
1157            expires_in: None,
1158            provider_id: context.provider_id.as_deref(),
1159            operation: context.operation_label(operation),
1160            anonymous: context.anonymous,
1161        };
1162
1163        self.finalize_sign_in(payload)
1164    }
1165
1166    pub(crate) async fn finalize_passkey_multi_factor_sign_in(
1167        self: &Arc<Self>,
1168        pending_credential: &str,
1169        enrollment_id: &str,
1170        response: WebAuthnAssertionResponse,
1171        context: Arc<MultiFactorSignInContext>,
1172        operation: MultiFactorOperation,
1173    ) -> AuthResult<UserCredential> {
1174        let api_key = self.api_key()?;
1175        let endpoint = self.identity_toolkit_endpoint();
1176        let request = FinalizePasskeyMfaSignInRequest {
1177            mfa_pending_credential: pending_credential.to_string(),
1178            mfa_enrollment_id: Some(enrollment_id.to_string()),
1179            webauthn_verification_info: WebAuthnVerificationInfo {
1180                payload: response.into_raw(),
1181            },
1182            tenant_id: None,
1183        };
1184
1185        let response =
1186            finalize_passkey_mfa_sign_in(&self.rest_client, &endpoint, &api_key, &request).await?;
1187
1188        let context = context.as_ref();
1189        let local_id = context.local_id.as_deref().ok_or_else(|| {
1190            AuthError::InvalidCredential("Missing localId for multi-factor sign-in".into())
1191        })?;
1192
1193        let provider_id = context.provider_id.as_deref().unwrap_or(WEBAUTHN_FACTOR_ID);
1194
1195        let payload = SignInResponsePayload {
1196            local_id,
1197            email: context.email.as_deref(),
1198            phone_number: context.phone_number.as_deref(),
1199            id_token: response.id_token.as_str(),
1200            refresh_token: response.refresh_token.as_str(),
1201            expires_in: None,
1202            provider_id: Some(provider_id),
1203            operation: context.operation_label(operation),
1204            anonymous: context.anonymous,
1205        };
1206
1207        self.finalize_sign_in(payload)
1208    }
1209
1210    pub(crate) async fn withdraw_multi_factor(&self, enrollment_id: &str) -> AuthResult<()> {
1211        let user = self.require_current_user()?;
1212        let id_token = user.get_id_token(false)?;
1213        let api_key = self.api_key()?;
1214        let endpoint = self.identity_toolkit_endpoint();
1215        let request = WithdrawMfaRequest {
1216            id_token: id_token.clone(),
1217            mfa_enrollment_id: enrollment_id.to_string(),
1218            tenant_id: None,
1219        };
1220
1221        let response = withdraw_mfa(&self.rest_client, &endpoint, &api_key, &request).await?;
1222        let new_id_token = response.id_token.unwrap_or(id_token);
1223        let new_refresh_token = response
1224            .refresh_token
1225            .or_else(|| user.refresh_token())
1226            .unwrap_or_default();
1227
1228        self.update_current_user_tokens(new_id_token, new_refresh_token, None)?;
1229        self.refresh_current_user_profile().await?;
1230        Ok(())
1231    }
1232
1233    fn endpoint_url(&self, path: &str, api_key: &str) -> AuthResult<Url> {
1234        let base = self.identity_toolkit_endpoint();
1235        let endpoint = format!("{}/{}?key={}", base.trim_end_matches('/'), path, api_key);
1236        Url::parse(&endpoint).map_err(|err| AuthError::Network(err.to_string()))
1237    }
1238
1239    fn build_user(
1240        &self,
1241        local_id: &str,
1242        email: Option<&str>,
1243        phone_number: Option<&str>,
1244        provider_id: Option<&str>,
1245    ) -> User {
1246        let info = UserInfo {
1247            uid: local_id.to_string(),
1248            display_name: None,
1249            email: email.map(|value| value.to_string()),
1250            phone_number: phone_number.map(|value| value.to_string()),
1251            photo_url: None,
1252            provider_id: provider_id
1253                .unwrap_or(EmailAuthProvider::PROVIDER_ID)
1254                .to_string(),
1255        };
1256        User::new(self.app.clone(), info)
1257    }
1258
1259    fn finalize_sign_in(&self, payload: SignInResponsePayload<'_>) -> AuthResult<UserCredential> {
1260        let SignInResponsePayload {
1261            local_id,
1262            email,
1263            phone_number,
1264            id_token,
1265            refresh_token,
1266            expires_in,
1267            provider_id,
1268            operation,
1269            anonymous,
1270        } = payload;
1271
1272        let mut user = self.build_user(local_id, email, phone_number, provider_id);
1273        user.set_anonymous(anonymous);
1274        let expiration = expires_in
1275            .map(|value| self.parse_expires_in(value))
1276            .transpose()?;
1277        user.update_tokens(
1278            Some(id_token.to_string()),
1279            Some(refresh_token.to_string()),
1280            expiration,
1281        );
1282        let user_arc = Arc::new(user);
1283        *self.current_user.lock().unwrap() = Some(user_arc.clone());
1284        self.after_token_update(user_arc.clone())?;
1285        self.listeners.notify(user_arc.clone());
1286
1287        Ok(UserCredential {
1288            user: user_arc,
1289            provider_id: provider_id.map(|id| id.to_string()),
1290            operation_type: Some(operation.to_string()),
1291        })
1292    }
1293
1294    pub(crate) async fn fetch_enrolled_factors(&self) -> AuthResult<Vec<MultiFactorInfo>> {
1295        let user = self.refresh_current_user_profile().await?;
1296        Ok(user.mfa_info())
1297    }
1298
1299    pub(crate) async fn multi_factor_session(&self) -> AuthResult<MultiFactorSession> {
1300        let user = self.require_current_user()?;
1301        let id_token = user.get_id_token(false)?;
1302        Ok(MultiFactorSession::enrollment(id_token))
1303    }
1304
1305    fn update_current_user_tokens(
1306        &self,
1307        id_token: String,
1308        refresh_token: String,
1309        expires_in: Option<Duration>,
1310    ) -> AuthResult<Arc<User>> {
1311        let user = self.require_current_user()?;
1312        user.update_tokens(Some(id_token), Some(refresh_token), expires_in);
1313        self.after_token_update(user.clone())?;
1314        Ok(user)
1315    }
1316
1317    async fn refresh_current_user_profile(&self) -> AuthResult<Arc<User>> {
1318        let Some(current) = self.current_user() else {
1319            return Err(AuthError::InvalidCredential("No user signed in".into()));
1320        };
1321
1322        let info = self.get_account_info().await?;
1323        let account = info
1324            .users
1325            .into_iter()
1326            .find(|user| user.local_id.as_deref() == Some(current.uid()))
1327            .ok_or_else(|| {
1328                AuthError::InvalidCredential("Account info missing current user".into())
1329            })?;
1330
1331        let provider_id = account
1332            .provider_user_info
1333            .as_ref()
1334            .and_then(|infos| infos.first())
1335            .and_then(|info| info.provider_id.clone())
1336            .unwrap_or_else(|| current.info().provider_id.clone());
1337
1338        let info = UserInfo {
1339            uid: account
1340                .local_id
1341                .clone()
1342                .unwrap_or_else(|| current.uid().to_string()),
1343            display_name: account
1344                .display_name
1345                .clone()
1346                .or_else(|| current.info().display_name.clone()),
1347            email: account
1348                .email
1349                .clone()
1350                .or_else(|| current.info().email.clone()),
1351            phone_number: account
1352                .phone_number
1353                .clone()
1354                .or_else(|| current.info().phone_number.clone()),
1355            photo_url: account
1356                .photo_url
1357                .clone()
1358                .or_else(|| current.info().photo_url.clone()),
1359            provider_id,
1360        };
1361
1362        let mut new_user = User::new(self.app.clone(), info);
1363        new_user.set_email_verified(account.email_verified.unwrap_or(current.email_verified()));
1364        new_user.set_anonymous(current.is_anonymous());
1365        let access_token = current.token_manager().access_token();
1366        let refresh_token = current.refresh_token();
1367        let expiration = current.token_manager().expiration_time();
1368        new_user
1369            .token_manager()
1370            .initialize(access_token, refresh_token, expiration);
1371
1372        if let Some(entries) = account.mfa_info.as_ref() {
1373            let factors = Self::convert_mfa_entries(entries);
1374            new_user.set_mfa_info(factors);
1375        }
1376
1377        let new_user = Arc::new(new_user);
1378        *self.current_user.lock().unwrap() = Some(new_user.clone());
1379        self.after_token_update(new_user.clone())?;
1380        self.listeners.notify(new_user.clone());
1381        Ok(new_user)
1382    }
1383
1384    fn convert_mfa_entries(entries: &[MfaEnrollmentInfo]) -> Vec<MultiFactorInfo> {
1385        let mut indexed: Vec<_> = entries
1386            .iter()
1387            .enumerate()
1388            .map(|(index, entry)| (index, Self::enrollment_timestamp(entry), entry))
1389            .collect();
1390
1391        indexed.sort_by(|(idx_a, ts_a, _), (idx_b, ts_b, _)| match (ts_a, ts_b) {
1392            (Some(a), Some(b)) => match a.cmp(b) {
1393                CmpOrdering::Equal => idx_a.cmp(idx_b),
1394                other => other,
1395            },
1396            (Some(_), None) => CmpOrdering::Less,
1397            (None, Some(_)) => CmpOrdering::Greater,
1398            (None, None) => idx_a.cmp(idx_b),
1399        });
1400
1401        indexed
1402            .into_iter()
1403            .filter_map(|(_, _, entry)| MultiFactorInfo::from_enrollment(entry))
1404            .collect()
1405    }
1406
1407    fn enrollment_timestamp(entry: &MfaEnrollmentInfo) -> Option<String> {
1408        entry.enrolled_at.as_ref().map(|value| match value {
1409            serde_json::Value::String(text) => text.clone(),
1410            other => other.to_string(),
1411        })
1412    }
1413
1414    fn build_multi_factor_error(
1415        &self,
1416        operation: MultiFactorOperation,
1417        pending_credential: String,
1418        info: Option<Vec<MfaEnrollmentInfo>>,
1419        context: MultiFactorSignInContext,
1420        user: Option<Arc<User>>,
1421    ) -> AuthError {
1422        let hints = info
1423            .as_ref()
1424            .map(|entries| Self::convert_mfa_entries(entries))
1425            .unwrap_or_else(Vec::new);
1426        let mut context = context;
1427        if context.provider_id.is_none() {
1428            if let Some(first) = hints.first() {
1429                context.provider_id = Some(first.factor_id.clone());
1430            }
1431        }
1432        let session = MultiFactorSession::sign_in(pending_credential);
1433        AuthError::MultiFactorRequired(MultiFactorError::new(
1434            operation, session, hints, context, user,
1435        ))
1436    }
1437
1438    async fn finalize_phone_confirmation(
1439        self: Arc<Self>,
1440        session_info: String,
1441        verification_code: String,
1442        flow: PhoneFinalization,
1443    ) -> AuthResult<UserCredential> {
1444        let api_key = self.api_key()?;
1445        let endpoint = self.identity_toolkit_endpoint();
1446        let mut request = SignInWithPhoneNumberRequest::default();
1447        request.session_info = Some(session_info);
1448        request.code = Some(verification_code);
1449
1450        let response = match &flow {
1451            PhoneFinalization::SignIn => {
1452                api_sign_in_with_phone_number(&self.rest_client, &endpoint, &api_key, &request)
1453                    .await?
1454            }
1455            PhoneFinalization::Link { id_token } => {
1456                request.id_token = Some(id_token.clone());
1457                api_link_with_phone_number(&self.rest_client, &endpoint, &api_key, &request).await?
1458            }
1459            PhoneFinalization::Reauth { id_token } => {
1460                request.id_token = Some(id_token.clone());
1461                verify_phone_number_for_existing(&self.rest_client, &endpoint, &api_key, &request)
1462                    .await?
1463            }
1464        };
1465
1466        self.handle_phone_response(response, flow).await
1467    }
1468
1469    async fn finalize_phone_credential(
1470        self: &Arc<Self>,
1471        credential: PhoneAuthCredential,
1472        flow: PhoneFinalization,
1473    ) -> AuthResult<UserCredential> {
1474        let (verification_id, verification_code) = credential.into_parts();
1475        self.clone()
1476            .finalize_phone_confirmation(verification_id, verification_code, flow)
1477            .await
1478    }
1479
1480    async fn handle_phone_response(
1481        &self,
1482        response: PhoneSignInResponse,
1483        flow: PhoneFinalization,
1484    ) -> AuthResult<UserCredential> {
1485        let PhoneSignInResponse {
1486            id_token,
1487            refresh_token,
1488            expires_in,
1489            local_id,
1490            is_new_user,
1491            phone_number,
1492            mfa_pending_credential,
1493            mfa_info,
1494            ..
1495        } = response;
1496
1497        if let Some(pending) = mfa_pending_credential {
1498            let mut context = MultiFactorSignInContext::default();
1499            context.local_id = local_id.clone();
1500            context.phone_number = phone_number.clone();
1501            context.provider_id = Some(PHONE_PROVIDER_ID.to_string());
1502            context.is_new_user = is_new_user;
1503
1504            let current_user = self.current_user();
1505            let (operation, user_for_error) = match flow {
1506                PhoneFinalization::SignIn => (MultiFactorOperation::SignIn, None),
1507                PhoneFinalization::Link { .. } => {
1508                    context.is_new_user = Some(false);
1509                    (MultiFactorOperation::Link, current_user.clone())
1510                }
1511                PhoneFinalization::Reauth { .. } => {
1512                    context.is_new_user = Some(false);
1513                    (MultiFactorOperation::Reauthenticate, current_user.clone())
1514                }
1515            };
1516
1517            if let Some(user) = user_for_error.as_ref() {
1518                context.anonymous = user.is_anonymous();
1519                if context.email.is_none() {
1520                    context.email = user.info().email.clone();
1521                }
1522            } else {
1523                context.anonymous = false;
1524            }
1525
1526            return Err(self.build_multi_factor_error(
1527                operation,
1528                pending,
1529                mfa_info,
1530                context,
1531                user_for_error,
1532            ));
1533        }
1534
1535        let local_id =
1536            local_id.ok_or_else(|| AuthError::InvalidCredential("Missing localId".into()))?;
1537        let id_token =
1538            id_token.ok_or_else(|| AuthError::InvalidCredential("Missing idToken".into()))?;
1539        let refresh_token = refresh_token
1540            .ok_or_else(|| AuthError::InvalidCredential("Missing refreshToken".into()))?;
1541        let is_new_user = is_new_user.unwrap_or(false);
1542
1543        let operation = match flow {
1544            PhoneFinalization::SignIn => {
1545                if is_new_user {
1546                    "signUp"
1547                } else {
1548                    "signIn"
1549                }
1550            }
1551            PhoneFinalization::Link { .. } => "link",
1552            PhoneFinalization::Reauth { .. } => "reauthenticate",
1553        };
1554
1555        let payload = SignInResponsePayload {
1556            local_id: local_id.as_str(),
1557            email: None,
1558            phone_number: phone_number.as_deref(),
1559            id_token: id_token.as_str(),
1560            refresh_token: refresh_token.as_str(),
1561            expires_in: expires_in.as_deref(),
1562            provider_id: Some(PHONE_PROVIDER_ID),
1563            operation,
1564            anonymous: false,
1565        };
1566
1567        let credential = self.finalize_sign_in(payload)?;
1568        self.refresh_current_user_profile().await?;
1569        Ok(credential)
1570    }
1571
1572    fn map_mfa_info(entries: &[MfaEnrollmentInfo]) -> Option<MultiFactorInfo> {
1573        entries
1574            .iter()
1575            .filter_map(MultiFactorInfo::from_enrollment)
1576            .next()
1577    }
1578
1579    fn api_key(&self) -> AuthResult<String> {
1580        self.config
1581            .lock()
1582            .unwrap()
1583            .api_key
1584            .clone()
1585            .ok_or_else(|| AuthError::InvalidCredential("Missing API key".into()))
1586    }
1587
1588    fn parse_expires_in(&self, value: &str) -> AuthResult<Duration> {
1589        let seconds = value.parse::<u64>().map_err(|err| {
1590            AuthError::InvalidCredential(format!("Invalid expiresIn value: {err}"))
1591        })?;
1592        Ok(Duration::from_secs(seconds))
1593    }
1594
1595    async fn refresh_user_token(&self, user: &Arc<User>) -> AuthResult<String> {
1596        let refresh_token = user
1597            .refresh_token()
1598            .ok_or_else(|| AuthError::InvalidCredential("Missing refresh token".into()))?;
1599        let api_key = self.api_key()?;
1600        let secure_endpoint = self.secure_token_endpoint();
1601        let response = token::refresh_id_token_with_endpoint(
1602            &self.rest_client,
1603            &secure_endpoint,
1604            &api_key,
1605            &refresh_token,
1606        )
1607        .await?;
1608        let expires_in = self.parse_expires_in(&response.expires_in)?;
1609        user.update_tokens(
1610            Some(response.id_token.clone()),
1611            Some(response.refresh_token.clone()),
1612            Some(expires_in),
1613        );
1614        self.after_token_update(user.clone())?;
1615        self.listeners.notify(user.clone());
1616        Ok(response.id_token)
1617    }
1618
1619    /// Returns the current user's ID token, refreshing when requested.
1620    pub async fn get_token(&self, force_refresh: bool) -> AuthResult<Option<String>> {
1621        let user = match self.current_user() {
1622            Some(user) => user,
1623            None => return Ok(None),
1624        };
1625
1626        let needs_refresh = force_refresh
1627            || user
1628                .token_manager()
1629                .should_refresh(self.token_refresh_tolerance);
1630
1631        if needs_refresh {
1632            let token = self.refresh_user_token(&user).await?;
1633            Ok(Some(token))
1634        } else {
1635            Ok(user.token_manager().access_token())
1636        }
1637    }
1638
1639    pub async fn get_token_async(&self, force_refresh: bool) -> AuthResult<Option<String>> {
1640        self.get_token(force_refresh).await
1641    }
1642
1643    /// Exposes this auth instance as a Firestore token provider.
1644    //#[cfg(feature = "firestore")]
1645    pub fn token_provider(self: &Arc<Self>) -> TokenProviderArc {
1646        crate::auth::token_provider::auth_token_provider_arc(self.clone())
1647    }
1648
1649    /// Overrides the default OAuth request URI used during flows.
1650    pub fn set_oauth_request_uri(&self, value: impl Into<String>) {
1651        *self.oauth_request_uri.lock().unwrap() = value.into();
1652    }
1653
1654    /// Returns the OAuth request URI for popup/redirect flows.
1655    pub fn oauth_request_uri(&self) -> String {
1656        self.oauth_request_uri.lock().unwrap().clone()
1657    }
1658
1659    /// Updates the Identity Toolkit REST endpoint.
1660    pub fn set_identity_toolkit_endpoint(&self, endpoint: impl Into<String>) {
1661        let value = endpoint.into();
1662        *self.identity_toolkit_endpoint.lock().unwrap() = value.clone();
1663        self.config.lock().unwrap().identity_toolkit_endpoint = Some(value);
1664    }
1665
1666    /// Returns the Identity Toolkit REST endpoint in use.
1667    pub fn identity_toolkit_endpoint(&self) -> String {
1668        self.identity_toolkit_endpoint.lock().unwrap().clone()
1669    }
1670
1671    /// Sets the Secure Token endpoint used for refresh operations.
1672    pub fn set_secure_token_endpoint(&self, endpoint: impl Into<String>) {
1673        let value = endpoint.into();
1674        *self.secure_token_endpoint.lock().unwrap() = value.clone();
1675        self.config.lock().unwrap().secure_token_endpoint = Some(value);
1676    }
1677
1678    fn secure_token_endpoint(&self) -> String {
1679        self.secure_token_endpoint.lock().unwrap().clone()
1680    }
1681
1682    /// Installs an OAuth popup handler implementation.
1683    pub fn set_popup_handler(&self, handler: Arc<dyn OAuthPopupHandler>) {
1684        *self.popup_handler.lock().unwrap() = Some(handler);
1685    }
1686
1687    /// Clears any installed popup handler.
1688    pub fn clear_popup_handler(&self) {
1689        *self.popup_handler.lock().unwrap() = None;
1690    }
1691
1692    /// Retrieves the currently configured popup handler.
1693    pub fn popup_handler(&self) -> Option<Arc<dyn OAuthPopupHandler>> {
1694        self.popup_handler.lock().unwrap().clone()
1695    }
1696
1697    /// Installs an OAuth redirect handler implementation.
1698    pub fn set_redirect_handler(&self, handler: Arc<dyn OAuthRedirectHandler>) {
1699        *self.redirect_handler.lock().unwrap() = Some(handler);
1700    }
1701
1702    /// Clears any installed redirect handler.
1703    pub fn clear_redirect_handler(&self) {
1704        *self.redirect_handler.lock().unwrap() = None;
1705    }
1706
1707    /// Retrieves the currently configured redirect handler.
1708    pub fn redirect_handler(&self) -> Option<Arc<dyn OAuthRedirectHandler>> {
1709        self.redirect_handler.lock().unwrap().clone()
1710    }
1711
1712    /// Replaces the persistence mechanism used for redirect state.
1713    pub fn set_redirect_persistence(&self, persistence: Arc<dyn RedirectPersistence>) {
1714        *self.redirect_persistence.lock().unwrap() = persistence;
1715    }
1716
1717    fn redirect_persistence(&self) -> Arc<dyn RedirectPersistence> {
1718        self.redirect_persistence.lock().unwrap().clone()
1719    }
1720
1721    /// Signs in using an OAuth credential produced by popup/redirect flows.
1722    pub async fn sign_in_with_oauth_credential(
1723        &self,
1724        credential: AuthCredential,
1725    ) -> AuthResult<UserCredential> {
1726        self.exchange_oauth_credential(credential, MultiFactorOperation::SignIn, None)
1727            .await
1728    }
1729
1730    /// Sends a password reset email to the specified address.
1731    pub async fn send_password_reset_email(&self, email: &str) -> AuthResult<()> {
1732        let api_key = self.api_key()?;
1733        let endpoint = self.identity_toolkit_endpoint();
1734        send_password_reset_email(&self.rest_client, &endpoint, &api_key, email).await
1735    }
1736
1737    /// Sends a sign-in link to the provided email address.
1738    ///
1739    /// # Examples
1740    /// ```rust,no_run
1741    /// # use firebase_rs_sdk::doctest_support::get_mock_auth;
1742    /// # use firebase_rs_sdk::auth::AuthError;
1743    /// # async fn run() -> Result<(), AuthError> {
1744    /// # let auth = get_mock_auth(None).await;
1745    /// let settings = firebase_rs_sdk::auth::ActionCodeSettings {
1746    ///     url: "https://example.com/finish".into(),
1747    ///     handle_code_in_app: true,
1748    ///     ..Default::default()
1749    /// };
1750    /// auth.send_sign_in_link_to_email("user@example.com", &settings).await?;
1751    /// # Ok(()) }
1752    /// ```
1753    pub async fn send_sign_in_link_to_email(
1754        &self,
1755        email: &str,
1756        settings: &ActionCodeSettings,
1757    ) -> AuthResult<()> {
1758        let api_key = self.api_key()?;
1759        let endpoint = self.identity_toolkit_endpoint();
1760        send_sign_in_link_to_email(&self.rest_client, &endpoint, &api_key, email, settings).await
1761    }
1762
1763    /// Confirms a password reset OOB code and applies the new password.
1764    pub async fn confirm_password_reset(
1765        &self,
1766        oob_code: &str,
1767        new_password: &str,
1768    ) -> AuthResult<()> {
1769        let api_key = self.api_key()?;
1770        let endpoint = self.identity_toolkit_endpoint();
1771        confirm_password_reset(
1772            &self.rest_client,
1773            &endpoint,
1774            &api_key,
1775            oob_code,
1776            new_password,
1777        )
1778        .await
1779    }
1780
1781    /// Sends an email verification message to the currently signed-in user.
1782    pub async fn send_email_verification(&self) -> AuthResult<()> {
1783        let user = self.require_current_user()?;
1784        let id_token = user.get_id_token(false)?;
1785        let api_key = self.api_key()?;
1786        let endpoint = self.identity_toolkit_endpoint();
1787        send_email_verification(&self.rest_client, &endpoint, &api_key, &id_token).await
1788    }
1789
1790    /// Returns `true` if the supplied link is an email sign-in link.
1791    pub fn is_sign_in_with_email_link(&self, email_link: &str) -> bool {
1792        ActionCodeUrl::parse(email_link)
1793            .map(|url| url.operation == ActionCodeOperation::EmailSignIn)
1794            .unwrap_or(false)
1795    }
1796
1797    /// Completes the email link sign-in flow for the given email address.
1798    ///
1799    /// # Examples
1800    /// ```rust,no_run
1801    /// # use firebase_rs_sdk::doctest_support::get_mock_auth;
1802    /// # use firebase_rs_sdk::auth::AuthError;
1803    /// # async fn run() -> Result<(), AuthError> {
1804    /// # let auth = get_mock_auth(None).await;
1805    /// let link = "link-to-sign-in";
1806    /// if auth.is_sign_in_with_email_link(&link) {
1807    ///     let credential = auth.sign_in_with_email_link("user@example.com", &link).await?;
1808    ///     println!("Signed in as {}", credential.user.uid());
1809    /// }
1810    /// # Ok(()) }
1811    /// ```
1812    pub async fn sign_in_with_email_link(
1813        &self,
1814        email: &str,
1815        email_link: &str,
1816    ) -> AuthResult<UserCredential> {
1817        let api_key = self.api_key()?;
1818        let action_url = ActionCodeUrl::parse(email_link)
1819            .ok_or_else(|| AuthError::InvalidCredential("Invalid email action link".into()))?;
1820
1821        if action_url.operation != ActionCodeOperation::EmailSignIn {
1822            return Err(AuthError::InvalidCredential(
1823                "Action link does not represent an email sign-in operation".into(),
1824            ));
1825        }
1826
1827        let request = SignInWithEmailLinkRequest {
1828            email: email.to_owned(),
1829            oob_code: action_url.code.clone(),
1830            return_secure_token: true,
1831            tenant_id: action_url.tenant_id.clone(),
1832            id_token: None,
1833        };
1834
1835        let response: SignInWithEmailLinkResponse = self
1836            .execute_request("accounts:signInWithEmailLink", &api_key, &request)
1837            .await?;
1838
1839        if let Some(pending) = response.mfa_pending_credential.clone() {
1840            let mut context = MultiFactorSignInContext::default();
1841            context.local_id = response.local_id.clone();
1842            context.email = response.email.clone().or_else(|| Some(email.to_owned()));
1843            context.provider_id = Some(EmailAuthProvider::PROVIDER_ID.to_string());
1844            context.anonymous = false;
1845
1846            return Err(self.build_multi_factor_error(
1847                MultiFactorOperation::SignIn,
1848                pending,
1849                response.mfa_info.clone(),
1850                context,
1851                None,
1852            ));
1853        }
1854
1855        let local_id = response
1856            .local_id
1857            .as_deref()
1858            .ok_or_else(|| AuthError::InvalidCredential("Missing localId".into()))?;
1859        let id_token = response
1860            .id_token
1861            .as_deref()
1862            .ok_or_else(|| AuthError::InvalidCredential("Missing idToken".into()))?;
1863        let refresh_token = response
1864            .refresh_token
1865            .as_deref()
1866            .ok_or_else(|| AuthError::InvalidCredential("Missing refreshToken".into()))?;
1867        let expires_in = response.expires_in.as_deref();
1868        let response_email = response.email.as_deref().unwrap_or(email);
1869        let operation = if response.is_new_user.unwrap_or(false) {
1870            "signUp"
1871        } else {
1872            "signIn"
1873        };
1874
1875        let payload = SignInResponsePayload {
1876            local_id,
1877            email: Some(response_email),
1878            phone_number: None,
1879            id_token,
1880            refresh_token,
1881            expires_in,
1882            provider_id: Some(EmailAuthProvider::PROVIDER_ID),
1883            operation,
1884            anonymous: false,
1885        };
1886
1887        self.finalize_sign_in(payload)
1888    }
1889
1890    /// Applies an out-of-band action code issued by Firebase Auth.
1891    ///
1892    /// # Examples
1893    /// ```rust,no_run
1894    /// # use firebase_rs_sdk::doctest_support::get_mock_auth;
1895    /// # use firebase_rs_sdk::auth::AuthError;
1896    /// # async fn run() -> Result<(), AuthError> {
1897    /// # let auth = get_mock_auth(None).await;
1898    /// auth.apply_action_code("ACTION_CODE_FROM_EMAIL").await?;
1899    /// # Ok(()) }
1900    /// ```
1901    pub async fn apply_action_code(&self, oob_code: &str) -> AuthResult<()> {
1902        let api_key = self.api_key()?;
1903        let endpoint = self.identity_toolkit_endpoint();
1904        apply_action_code(&self.rest_client, &endpoint, &api_key, oob_code, None).await
1905    }
1906
1907    /// Retrieves metadata describing the provided action code.
1908    ///
1909    /// # Examples
1910    /// ```rust,no_run
1911    /// # use firebase_rs_sdk::doctest_support::get_mock_auth;
1912    /// # use firebase_rs_sdk::auth::AuthError;
1913    /// # async fn run() -> Result<(), AuthError> {
1914    /// # let auth = get_mock_auth(None).await;
1915    /// let info = auth.check_action_code("ACTION_CODE").await?;
1916    /// println!("Operation: {:?}", info.operation);
1917    /// # Ok(()) }
1918    /// ```
1919    pub async fn check_action_code(&self, oob_code: &str) -> AuthResult<ActionCodeInfo> {
1920        let api_key = self.api_key()?;
1921        let endpoint = self.identity_toolkit_endpoint();
1922        let response =
1923            reset_password_info(&self.rest_client, &endpoint, &api_key, oob_code, None).await?;
1924
1925        let request_type = response
1926            .request_type
1927            .as_deref()
1928            .ok_or_else(|| AuthError::InvalidCredential("Missing requestType".into()))?;
1929        let operation = ActionCodeOperation::from_request_type(request_type).ok_or_else(|| {
1930            AuthError::InvalidCredential(format!("Unknown requestType: {request_type}"))
1931        })?;
1932
1933        let email = if operation == ActionCodeOperation::VerifyAndChangeEmail {
1934            response.new_email.as_deref()
1935        } else {
1936            response.email.as_deref()
1937        };
1938        let previous_email = if operation == ActionCodeOperation::VerifyAndChangeEmail {
1939            response.email.as_deref()
1940        } else {
1941            response.new_email.as_deref()
1942        };
1943
1944        let multi_factor_info = response
1945            .mfa_info
1946            .as_ref()
1947            .and_then(|infos| Self::map_mfa_info(infos));
1948
1949        let data = ActionCodeInfoData {
1950            email: email.map(|value| value.to_string()),
1951            previous_email: previous_email.map(|value| value.to_string()),
1952            multi_factor_info,
1953            from_email: None,
1954        };
1955
1956        Ok(ActionCodeInfo { data, operation })
1957    }
1958
1959    /// Returns the email address associated with the provided password reset code.
1960    ///
1961    /// # Examples
1962    /// ```rust,no_run
1963    /// # use firebase_rs_sdk::doctest_support::get_mock_auth;
1964    /// # use firebase_rs_sdk::auth::AuthError;
1965    /// # async fn run() -> Result<(), AuthError> {
1966    /// # let auth = get_mock_auth(None).await;
1967    /// let email = auth.verify_password_reset_code("RESET_CODE").await?;
1968    /// println!("Reset applies to {email}");
1969    /// # Ok(()) }
1970    /// ```
1971    pub async fn verify_password_reset_code(&self, oob_code: &str) -> AuthResult<String> {
1972        let info = self.check_action_code(oob_code).await?;
1973        info.data
1974            .email
1975            .clone()
1976            .ok_or_else(|| AuthError::InvalidCredential("Action code missing email".into()))
1977    }
1978
1979    /// Updates the current user's display name and photo URL.
1980    pub async fn update_profile(
1981        &self,
1982        display_name: Option<&str>,
1983        photo_url: Option<&str>,
1984    ) -> AuthResult<Arc<User>> {
1985        let user = self.require_current_user()?;
1986        let id_token = user.get_id_token(false)?;
1987        let mut request = UpdateAccountRequest::new(id_token);
1988        if let Some(value) = display_name {
1989            if value.is_empty() {
1990                request.display_name = Some(UpdateString::Clear);
1991            } else {
1992                request.display_name = Some(UpdateString::Set(value.to_string()));
1993            }
1994        }
1995        if let Some(value) = photo_url {
1996            if value.is_empty() {
1997                request.photo_url = Some(UpdateString::Clear);
1998            } else {
1999                request.photo_url = Some(UpdateString::Set(value.to_string()));
2000            }
2001        }
2002
2003        self.perform_account_update(user, request).await
2004    }
2005
2006    /// Updates the current user's email address.
2007    pub async fn update_email(&self, email: &str) -> AuthResult<Arc<User>> {
2008        let user = self.require_current_user()?;
2009        let id_token = user.get_id_token(false)?;
2010        let mut request = UpdateAccountRequest::new(id_token);
2011        request.email = Some(email.to_string());
2012        self.perform_account_update(user, request).await
2013    }
2014
2015    /// Updates the current user's password.
2016    pub async fn update_password(&self, password: &str) -> AuthResult<Arc<User>> {
2017        let user = self.require_current_user()?;
2018        let id_token = user.get_id_token(false)?;
2019        let mut request = UpdateAccountRequest::new(id_token);
2020        request.password = Some(password.to_string());
2021        self.perform_account_update(user, request).await
2022    }
2023
2024    /// Deletes the current user from Firebase Auth.
2025    pub async fn delete_user(&self) -> AuthResult<()> {
2026        let user = self.require_current_user()?;
2027        let id_token = user.get_id_token(false)?;
2028        let api_key = self.api_key()?;
2029        let endpoint = self.identity_toolkit_endpoint();
2030        delete_account(&self.rest_client, &endpoint, &api_key, &id_token).await?;
2031        self.sign_out();
2032        Ok(())
2033    }
2034
2035    /// Unlinks the specified providers from the current user.
2036    pub async fn unlink_providers(&self, provider_ids: &[&str]) -> AuthResult<Arc<User>> {
2037        let user = self.require_current_user()?;
2038        let id_token = user.get_id_token(false)?;
2039        let mut request = UpdateAccountRequest::new(id_token);
2040        request.delete_providers = provider_ids.iter().map(|id| id.to_string()).collect();
2041        self.perform_account_update(user, request).await
2042    }
2043
2044    /// Fetches the latest account info for the current user.
2045    pub async fn get_account_info(&self) -> AuthResult<GetAccountInfoResponse> {
2046        let user = self.require_current_user()?;
2047        let id_token = user.get_id_token(false)?;
2048        let api_key = self.api_key()?;
2049        let endpoint = self.identity_toolkit_endpoint();
2050        get_account_info(&self.rest_client, &endpoint, &api_key, &id_token).await
2051    }
2052
2053    /// Links an OAuth credential with the currently signed-in user.
2054    pub async fn link_with_oauth_credential(
2055        &self,
2056        credential: AuthCredential,
2057    ) -> AuthResult<UserCredential> {
2058        let user = self.require_current_user()?;
2059        let id_token = user.get_id_token(false)?;
2060        self.exchange_oauth_credential(credential, MultiFactorOperation::Link, Some(id_token))
2061            .await
2062    }
2063
2064    /// Reauthenticates the current user with email and password.
2065    pub async fn reauthenticate_with_password(
2066        &self,
2067        email: &str,
2068        password: &str,
2069    ) -> AuthResult<Arc<User>> {
2070        let request = SignInWithPasswordRequest {
2071            email: email.to_string(),
2072            password: password.to_string(),
2073            return_secure_token: true,
2074        };
2075
2076        let api_key = self.api_key()?;
2077        let endpoint = self.identity_toolkit_endpoint();
2078        let response = verify_password(&self.rest_client, &endpoint, &api_key, &request).await?;
2079        self.apply_password_reauth(response)
2080    }
2081
2082    /// Reauthenticates the current user with an OAuth credential.
2083    pub async fn reauthenticate_with_oauth_credential(
2084        &self,
2085        credential: AuthCredential,
2086    ) -> AuthResult<Arc<User>> {
2087        let user = self.require_current_user()?;
2088        let result = self
2089            .exchange_oauth_credential(
2090                credential,
2091                MultiFactorOperation::Reauthenticate,
2092                Some(user.get_id_token(false)?),
2093            )
2094            .await?;
2095        Ok(result.user)
2096    }
2097
2098    async fn exchange_oauth_credential(
2099        &self,
2100        credential: AuthCredential,
2101        operation: MultiFactorOperation,
2102        id_token: Option<String>,
2103    ) -> AuthResult<UserCredential> {
2104        let oauth_credential = OAuthCredential::try_from(credential)?;
2105        let post_body = oauth_credential.build_post_body()?;
2106        let request = SignInWithIdpRequest {
2107            post_body,
2108            request_uri: self.oauth_request_uri(),
2109            return_idp_credential: true,
2110            return_secure_token: true,
2111            id_token,
2112        };
2113
2114        let api_key = self.api_key()?;
2115        let response = sign_in_with_idp(&self.rest_client, &api_key, &request).await?;
2116        if let Some(pending) = response.mfa_pending_credential.clone() {
2117            let mut context = MultiFactorSignInContext::default();
2118            context.local_id = response.local_id.clone();
2119            context.email = response.email.clone();
2120            context.provider_id = response
2121                .provider_id
2122                .clone()
2123                .or_else(|| Some(oauth_credential.provider_id().to_string()));
2124            context.is_new_user = match operation {
2125                MultiFactorOperation::SignIn => response.is_new_user,
2126                _ => Some(false),
2127            };
2128
2129            let user_for_error = if operation == MultiFactorOperation::SignIn {
2130                None
2131            } else {
2132                self.current_user()
2133            };
2134
2135            if let Some(user) = user_for_error.as_ref() {
2136                context.anonymous = user.is_anonymous();
2137                if context.email.is_none() {
2138                    context.email = user.info().email.clone();
2139                }
2140            }
2141
2142            return Err(self.build_multi_factor_error(
2143                operation,
2144                pending,
2145                response.mfa_info.clone(),
2146                context,
2147                user_for_error,
2148            ));
2149        }
2150        let user_arc = self.upsert_user_from_idp_response(&response, &oauth_credential)?;
2151        let provider_id = response
2152            .provider_id
2153            .clone()
2154            .or_else(|| Some(oauth_credential.provider_id().to_string()))
2155            .unwrap_or_else(|| EmailAuthProvider::PROVIDER_ID.to_string());
2156
2157        self.listeners.notify(user_arc.clone());
2158
2159        Ok(UserCredential {
2160            user: user_arc,
2161            provider_id: Some(provider_id),
2162            operation_type: Some(if response.is_new_user.unwrap_or(false) {
2163                "signUp".to_string()
2164            } else {
2165                "signIn".to_string()
2166            }),
2167        })
2168    }
2169
2170    async fn perform_account_update(
2171        &self,
2172        current_user: Arc<User>,
2173        request: UpdateAccountRequest,
2174    ) -> AuthResult<Arc<User>> {
2175        let api_key = self.api_key()?;
2176        let endpoint = self.identity_toolkit_endpoint();
2177        let response = update_account(&self.rest_client, &endpoint, &api_key, &request).await?;
2178        let updated_user = self.apply_account_update(&current_user, &response)?;
2179        self.listeners.notify(updated_user.clone());
2180        Ok(updated_user)
2181    }
2182
2183    fn require_current_user(&self) -> AuthResult<Arc<User>> {
2184        self.current_user()
2185            .ok_or_else(|| AuthError::InvalidCredential("No user signed in".into()))
2186    }
2187
2188    pub(crate) fn set_pending_redirect_event(
2189        &self,
2190        provider_id: &str,
2191        operation: RedirectOperation,
2192        pkce_verifier: Option<String>,
2193    ) -> AuthResult<()> {
2194        let event = PendingRedirectEvent {
2195            provider_id: provider_id.to_string(),
2196            operation,
2197            pkce_verifier,
2198        };
2199        self.redirect_persistence().set(Some(event))
2200    }
2201
2202    pub(crate) fn clear_pending_redirect_event(&self) -> AuthResult<()> {
2203        self.redirect_persistence().set(None)
2204    }
2205
2206    pub(crate) fn take_pending_redirect_event(&self) -> AuthResult<Option<PendingRedirectEvent>> {
2207        let event = self.redirect_persistence().get()?;
2208        if event.is_some() {
2209            self.redirect_persistence().set(None)?;
2210        }
2211        Ok(event)
2212    }
2213
2214    fn apply_password_reauth(&self, response: SignInWithPasswordResponse) -> AuthResult<Arc<User>> {
2215        let current_user = self.current_user();
2216
2217        if let Some(pending) = response.mfa_pending_credential.clone() {
2218            let mut context = MultiFactorSignInContext::default();
2219            context.local_id = Some(response.local_id.clone());
2220            context.email = Some(response.email.clone());
2221            context.provider_id = Some(EmailAuthProvider::PROVIDER_ID.to_string());
2222            context.anonymous = false;
2223
2224            return Err(self.build_multi_factor_error(
2225                MultiFactorOperation::Reauthenticate,
2226                pending,
2227                response.mfa_info.clone(),
2228                context,
2229                current_user,
2230            ));
2231        }
2232
2233        let mut user = self.build_user(
2234            &response.local_id,
2235            Some(&response.email),
2236            None,
2237            Some(EmailAuthProvider::PROVIDER_ID),
2238        );
2239        user.set_anonymous(false);
2240        let expires_in = response
2241            .expires_in
2242            .as_deref()
2243            .ok_or_else(|| AuthError::InvalidCredential("Missing expiresIn".into()))?;
2244        let expires_in = self.parse_expires_in(expires_in)?;
2245        let id_token = response
2246            .id_token
2247            .as_ref()
2248            .ok_or_else(|| AuthError::InvalidCredential("Missing idToken".into()))?
2249            .clone();
2250        let refresh_token = response
2251            .refresh_token
2252            .as_ref()
2253            .ok_or_else(|| AuthError::InvalidCredential("Missing refreshToken".into()))?
2254            .clone();
2255        user.update_tokens(Some(id_token), Some(refresh_token), Some(expires_in));
2256
2257        let user_arc = Arc::new(user);
2258        *self.current_user.lock().unwrap() = Some(user_arc.clone());
2259        self.after_token_update(user_arc.clone())?;
2260        Ok(user_arc)
2261    }
2262
2263    fn restore_from_persistence(&self) -> AuthResult<()> {
2264        let state = self.persistence.get()?;
2265        let notify = state.is_some();
2266        self.sync_from_persistence(state, notify)
2267    }
2268
2269    fn after_token_update(&self, user: Arc<User>) -> AuthResult<()> {
2270        self.save_persisted_state(&user)?;
2271        self.schedule_refresh_for_user(user);
2272        Ok(())
2273    }
2274
2275    fn save_persisted_state(&self, user: &Arc<User>) -> AuthResult<()> {
2276        let refresh_token = match user.refresh_token() {
2277            Some(token) if !token.is_empty() => Some(token),
2278            _ => {
2279                self.set_persisted_state(None)?;
2280                return Ok(());
2281            }
2282        };
2283
2284        let expires_at = user
2285            .token_manager()
2286            .expiration_time()
2287            .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
2288            .map(|duration| duration.as_secs() as i64);
2289
2290        let state = PersistedAuthState {
2291            user_id: user.uid().to_string(),
2292            email: user.info().email.clone(),
2293            refresh_token,
2294            access_token: user.token_manager().access_token(),
2295            expires_at,
2296        };
2297        self.set_persisted_state(Some(state))
2298    }
2299
2300    fn set_persisted_state(&self, state: Option<PersistedAuthState>) -> AuthResult<()> {
2301        {
2302            let cache = self.persisted_state_cache.lock().unwrap();
2303            if *cache == state {
2304                return Ok(());
2305            }
2306        }
2307
2308        let previous = self.update_cached_state(state.clone());
2309        if let Err(err) = self.persistence.set(state) {
2310            self.update_cached_state(previous);
2311            return Err(err);
2312        }
2313        Ok(())
2314    }
2315
2316    fn update_cached_state(&self, state: Option<PersistedAuthState>) -> Option<PersistedAuthState> {
2317        let mut guard = self.persisted_state_cache.lock().unwrap();
2318        std::mem::replace(&mut *guard, state)
2319    }
2320
2321    fn install_persistence_subscription(self: &Arc<Self>) -> AuthResult<()> {
2322        let weak = Arc::downgrade(self);
2323        let listener: PersistenceListener = Arc::new(move |state: Option<PersistedAuthState>| {
2324            if let Some(auth) = weak.upgrade() {
2325                if let Err(err) = auth.sync_from_persistence(state, true) {
2326                    eprintln!("Failed to sync persisted auth state: {err}");
2327                }
2328            }
2329        });
2330
2331        let subscription = self.persistence.subscribe(listener)?;
2332        *self.persistence_subscription.lock().unwrap() = Some(subscription);
2333        Ok(())
2334    }
2335
2336    fn sync_from_persistence(
2337        &self,
2338        state: Option<PersistedAuthState>,
2339        notify_listeners: bool,
2340    ) -> AuthResult<()> {
2341        {
2342            let cache = self.persisted_state_cache.lock().unwrap();
2343            if *cache == state {
2344                return Ok(());
2345            }
2346        }
2347
2348        match state.clone() {
2349            Some(ref persisted) if Self::has_refresh_token(persisted) => {
2350                let user_arc = self.build_user_from_persisted_state(persisted);
2351                *self.current_user.lock().unwrap() = Some(user_arc.clone());
2352                self.schedule_refresh_for_user(user_arc.clone());
2353                if notify_listeners {
2354                    self.listeners.notify(user_arc);
2355                }
2356            }
2357            _ => {
2358                self.clear_local_user_state();
2359            }
2360        }
2361
2362        self.update_cached_state(state);
2363        Ok(())
2364    }
2365
2366    fn build_user_from_persisted_state(&self, state: &PersistedAuthState) -> Arc<User> {
2367        let info = UserInfo {
2368            uid: state.user_id.clone(),
2369            display_name: None,
2370            email: state.email.clone(),
2371            phone_number: None,
2372            photo_url: None,
2373            provider_id: EmailAuthProvider::PROVIDER_ID.to_string(),
2374        };
2375
2376        let user = User::new(self.app.clone(), info);
2377        let expiration_time = state.expires_at.and_then(|seconds| {
2378            if seconds <= 0 {
2379                None
2380            } else {
2381                UNIX_EPOCH.checked_add(Duration::from_secs(seconds as u64))
2382            }
2383        });
2384
2385        user.token_manager().initialize(
2386            state.access_token.clone(),
2387            state.refresh_token.clone(),
2388            expiration_time,
2389        );
2390
2391        Arc::new(user)
2392    }
2393
2394    fn clear_local_user_state(&self) {
2395        self.cancel_scheduled_refresh();
2396        let mut guard = self.current_user.lock().unwrap();
2397        if let Some(user) = guard.as_ref() {
2398            user.token_manager().clear();
2399        }
2400        *guard = None;
2401    }
2402
2403    fn has_refresh_token(state: &PersistedAuthState) -> bool {
2404        state
2405            .refresh_token
2406            .as_ref()
2407            .map(|token| !token.is_empty())
2408            .unwrap_or(false)
2409    }
2410
2411    fn upsert_user_from_idp_response(
2412        &self,
2413        response: &SignInWithIdpResponse,
2414        oauth_credential: &OAuthCredential,
2415    ) -> AuthResult<Arc<User>> {
2416        let id_token = response.id_token.clone().ok_or_else(|| {
2417            AuthError::InvalidCredential("signInWithIdp response missing idToken".into())
2418        })?;
2419        let refresh_token = response.refresh_token.clone().ok_or_else(|| {
2420            AuthError::InvalidCredential("signInWithIdp response missing refreshToken".into())
2421        })?;
2422        let local_id = response.local_id.clone().ok_or_else(|| {
2423            AuthError::InvalidCredential("signInWithIdp response missing localId".into())
2424        })?;
2425
2426        let provider_id = response
2427            .provider_id
2428            .clone()
2429            .unwrap_or_else(|| oauth_credential.provider_id().to_string());
2430
2431        let display_name = oauth_credential
2432            .token_response()
2433            .get("displayName")
2434            .and_then(Value::as_str)
2435            .map(|value| value.to_string());
2436        let photo_url = oauth_credential
2437            .token_response()
2438            .get("photoUrl")
2439            .and_then(Value::as_str)
2440            .map(|value| value.to_string());
2441
2442        let info = UserInfo {
2443            uid: local_id,
2444            display_name,
2445            email: response.email.clone(),
2446            phone_number: None,
2447            photo_url,
2448            provider_id,
2449        };
2450
2451        let user = User::new(self.app.clone(), info);
2452        let expires_in = response
2453            .expires_in
2454            .as_deref()
2455            .map(|value| self.parse_expires_in(value))
2456            .transpose()?;
2457        user.update_tokens(Some(id_token), Some(refresh_token), expires_in);
2458
2459        let user_arc = Arc::new(user);
2460        *self.current_user.lock().unwrap() = Some(user_arc.clone());
2461        self.after_token_update(user_arc.clone())?;
2462        Ok(user_arc)
2463    }
2464
2465    fn apply_account_update(
2466        &self,
2467        current_user: &Arc<User>,
2468        response: &UpdateAccountResponse,
2469    ) -> AuthResult<Arc<User>> {
2470        let id_token = response.id_token.clone().ok_or_else(|| {
2471            AuthError::InvalidCredential("accounts:update response missing idToken".into())
2472        })?;
2473        let refresh_token = response.refresh_token.clone().ok_or_else(|| {
2474            AuthError::InvalidCredential("accounts:update response missing refreshToken".into())
2475        })?;
2476
2477        let expires_in = response
2478            .expires_in
2479            .as_deref()
2480            .map(|value| self.parse_expires_in(value))
2481            .transpose()?;
2482
2483        let uid = response
2484            .local_id
2485            .clone()
2486            .unwrap_or_else(|| current_user.uid().to_string());
2487
2488        let email = response
2489            .email
2490            .clone()
2491            .or_else(|| current_user.info().email.clone());
2492
2493        let display_name = match response.display_name.as_deref() {
2494            Some(value) if value.is_empty() => None,
2495            Some(value) => Some(value.to_string()),
2496            None => current_user.info().display_name.clone(),
2497        };
2498
2499        let photo_url = match response.photo_url.as_deref() {
2500            Some(value) if value.is_empty() => None,
2501            Some(value) => Some(value.to_string()),
2502            None => current_user.info().photo_url.clone(),
2503        };
2504
2505        let provider_id = response
2506            .provider_user_info
2507            .as_ref()
2508            .and_then(|infos| infos.first())
2509            .and_then(|info| info.provider_id.clone())
2510            .unwrap_or_else(|| current_user.info().provider_id.clone());
2511
2512        let info = UserInfo {
2513            uid,
2514            display_name,
2515            email,
2516            phone_number: current_user.info().phone_number.clone(),
2517            photo_url,
2518            provider_id,
2519        };
2520
2521        let mut user = User::new(self.app.clone(), info);
2522        user.set_email_verified(current_user.email_verified());
2523        if let Some(entries) = response.mfa_info.as_ref() {
2524            let factors = Self::convert_mfa_entries(entries);
2525            user.set_mfa_info(factors);
2526        }
2527        user.update_tokens(Some(id_token), Some(refresh_token), expires_in);
2528
2529        let user_arc = Arc::new(user);
2530        *self.current_user.lock().unwrap() = Some(user_arc.clone());
2531        self.after_token_update(user_arc.clone())?;
2532        Ok(user_arc)
2533    }
2534
2535    fn schedule_refresh_for_user(&self, user: Arc<User>) {
2536        self.cancel_scheduled_refresh();
2537
2538        let Some(expiration) = user.token_manager().expiration_time() else {
2539            return;
2540        };
2541
2542        let trigger_time = expiration
2543            .checked_sub(self.token_refresh_tolerance)
2544            .unwrap_or(expiration);
2545        let now = SystemTime::now();
2546        let delay = trigger_time
2547            .duration_since(now)
2548            .unwrap_or(Duration::from_secs(0));
2549
2550        let cancel_flag = Arc::new(AtomicBool::new(false));
2551        *self.refresh_cancel.lock().unwrap() = Some(cancel_flag.clone());
2552
2553        let Some(auth_arc) = self.self_arc() else {
2554            return;
2555        };
2556
2557        let user_for_refresh = user.clone();
2558        spawn_detached(async move {
2559            if !delay.is_zero() {
2560                runtime_sleep(delay).await;
2561            }
2562
2563            if cancel_flag.load(Ordering::SeqCst) {
2564                return;
2565            }
2566
2567            if let Err(err) = auth_arc.refresh_user_token(&user_for_refresh).await {
2568                APP_LOGGER.warn(format!("Failed to refresh Auth token: {err}"));
2569            }
2570        });
2571    }
2572
2573    fn cancel_scheduled_refresh(&self) {
2574        if let Some(flag) = self.refresh_cancel.lock().unwrap().take() {
2575            flag.store(true, Ordering::SeqCst);
2576        }
2577    }
2578
2579    fn self_arc(&self) -> Option<Arc<Auth>> {
2580        self.self_ref.lock().unwrap().upgrade()
2581    }
2582}
2583
2584pub struct AuthBuilder {
2585    app: FirebaseApp,
2586    persistence: Option<Arc<dyn AuthPersistence + Send + Sync>>,
2587    auto_initialize: bool,
2588    popup_handler: Option<Arc<dyn OAuthPopupHandler>>,
2589    redirect_handler: Option<Arc<dyn OAuthRedirectHandler>>,
2590    oauth_request_uri: Option<String>,
2591    redirect_persistence: Option<Arc<dyn RedirectPersistence>>,
2592    identity_toolkit_endpoint: Option<String>,
2593    secure_token_endpoint: Option<String>,
2594}
2595
2596impl AuthBuilder {
2597    fn new(app: FirebaseApp) -> Self {
2598        Self {
2599            app,
2600            persistence: None,
2601            auto_initialize: true,
2602            popup_handler: None,
2603            redirect_handler: None,
2604            oauth_request_uri: None,
2605            redirect_persistence: None,
2606            identity_toolkit_endpoint: None,
2607            secure_token_endpoint: None,
2608        }
2609    }
2610
2611    /// Overrides the persistence backend used by the Auth instance.
2612    pub fn with_persistence(mut self, persistence: Arc<dyn AuthPersistence + Send + Sync>) -> Self {
2613        self.persistence = Some(persistence);
2614        self
2615    }
2616
2617    /// Installs a popup handler prior to building the Auth instance.
2618    pub fn with_popup_handler(mut self, handler: Arc<dyn OAuthPopupHandler>) -> Self {
2619        self.popup_handler = Some(handler);
2620        self
2621    }
2622
2623    /// Installs a redirect handler prior to building the Auth instance.
2624    pub fn with_redirect_handler(mut self, handler: Arc<dyn OAuthRedirectHandler>) -> Self {
2625        self.redirect_handler = Some(handler);
2626        self
2627    }
2628
2629    /// Overrides the default OAuth request URI before building.
2630    pub fn with_oauth_request_uri(mut self, request_uri: impl Into<String>) -> Self {
2631        self.oauth_request_uri = Some(request_uri.into());
2632        self
2633    }
2634
2635    /// Configures the redirect persistence implementation used post-build.
2636    pub fn with_redirect_persistence(mut self, persistence: Arc<dyn RedirectPersistence>) -> Self {
2637        self.redirect_persistence = Some(persistence);
2638        self
2639    }
2640
2641    /// Overrides the Identity Toolkit endpoint used by the Auth instance.
2642    pub fn with_identity_toolkit_endpoint(mut self, endpoint: impl Into<String>) -> Self {
2643        self.identity_toolkit_endpoint = Some(endpoint.into());
2644        self
2645    }
2646
2647    /// Overrides the Secure Token endpoint used for refresh operations.
2648    pub fn with_secure_token_endpoint(mut self, endpoint: impl Into<String>) -> Self {
2649        self.secure_token_endpoint = Some(endpoint.into());
2650        self
2651    }
2652
2653    /// Prevents `build` from automatically calling `initialize`.
2654    pub fn defer_initialization(mut self) -> Self {
2655        self.auto_initialize = false;
2656        self
2657    }
2658
2659    /// Builds the Auth instance, applying all configured overrides.
2660    pub fn build(self) -> AuthResult<Arc<Auth>> {
2661        let persistence = self
2662            .persistence
2663            .unwrap_or_else(|| Arc::new(InMemoryPersistence::default()));
2664        let auth = Arc::new(Auth::new_with_persistence(self.app, persistence)?);
2665        if let Some(handler) = self.popup_handler {
2666            auth.set_popup_handler(handler);
2667        }
2668        if let Some(handler) = self.redirect_handler {
2669            auth.set_redirect_handler(handler);
2670        }
2671        if let Some(request_uri) = self.oauth_request_uri {
2672            auth.set_oauth_request_uri(request_uri);
2673        }
2674        if let Some(persistence) = self.redirect_persistence {
2675            auth.set_redirect_persistence(persistence);
2676        }
2677        if let Some(endpoint) = self.identity_toolkit_endpoint {
2678            auth.set_identity_toolkit_endpoint(endpoint);
2679        }
2680        if let Some(endpoint) = self.secure_token_endpoint {
2681            auth.set_secure_token_endpoint(endpoint);
2682        }
2683        if self.auto_initialize {
2684            auth.initialize()?;
2685        }
2686        Ok(auth)
2687    }
2688}
2689
2690/// Registers the Auth component so apps can resolve `Auth` instances.
2691pub fn register_auth_component() {
2692    use std::sync::LazyLock;
2693    static REGISTERED: LazyLock<()> = LazyLock::new(|| {
2694        let component = Component::new("auth", Arc::new(auth_factory), ComponentType::Public)
2695            .with_instantiation_mode(InstantiationMode::Lazy);
2696        let _ = register_component(component);
2697    });
2698    LazyLock::force(&REGISTERED);
2699}
2700
2701fn auth_factory(
2702    container: &ComponentContainer,
2703    _options: InstanceFactoryOptions,
2704) -> Result<DynService, ComponentError> {
2705    let app = container.root_service::<FirebaseApp>().ok_or_else(|| {
2706        ComponentError::InitializationFailed {
2707            name: "auth".to_string(),
2708            reason: "Firebase app not attached to component container".to_string(),
2709        }
2710    })?;
2711    let auth = Auth::new((*app).clone()).map_err(|err| ComponentError::InitializationFailed {
2712        name: "auth".to_string(),
2713        reason: err.to_string(),
2714    })?;
2715    let auth = Arc::new(auth);
2716    auth.initialize()
2717        .map_err(|err| ComponentError::InitializationFailed {
2718            name: "auth".to_string(),
2719            reason: err.to_string(),
2720        })?;
2721    Ok(auth as DynService)
2722}
2723
2724/// Retrieves the `Auth` service for the provided app, initializing if needed.
2725pub fn auth_for_app(app: FirebaseApp) -> AuthResult<Arc<Auth>> {
2726    let provider = app.container().get_provider("auth");
2727    provider.get_immediate::<Auth>().ok_or_else(|| {
2728        AuthError::App(AppError::ComponentFailure {
2729            component: "auth".to_string(),
2730            message: "Auth service not initialized".to_string(),
2731        })
2732    })
2733}
2734
2735#[cfg(all(test, not(target_arch = "wasm32")))]
2736mod tests {
2737    use super::*;
2738    use crate::auth::error::MultiFactorAuthErrorCode;
2739    use crate::auth::types::{
2740        ActionCodeSettings, AndroidSettings, ApplicationVerifier, IosSettings,
2741    };
2742    use crate::auth::{
2743        get_multi_factor_resolver, FirebaseAuth, PhoneAuthProvider, PhoneMultiFactorGenerator,
2744        TotpMultiFactorGenerator, WebAuthnAssertionResponse, WebAuthnAttestationResponse,
2745        WebAuthnMultiFactorGenerator, WEBAUTHN_FACTOR_ID,
2746    };
2747    use crate::test_support::{start_mock_server, test_firebase_app_with_api_key};
2748    use httpmock::prelude::*;
2749    use serde_json::json;
2750
2751    const TEST_API_KEY: &str = "test-api-key";
2752    const TEST_EMAIL: &str = "user@example.com";
2753    const TEST_PASSWORD: &str = "secret";
2754    const TEST_UID: &str = "uid-123";
2755    const TEST_ID_TOKEN: &str = "id-token";
2756    const TEST_REFRESH_TOKEN: &str = "refresh-token";
2757    const REAUTH_EMAIL: &str = "reauth@example.com";
2758    const REAUTH_ID_TOKEN: &str = "reauth-id-token";
2759    const REAUTH_REFRESH_TOKEN: &str = "reauth-refresh-token";
2760    const REAUTH_UID: &str = "reauth-uid";
2761    const GOOGLE_PROVIDER_ID: &str = "google.com";
2762    const UPDATED_ID_TOKEN: &str = "updated-id-token";
2763    const UPDATED_REFRESH_TOKEN: &str = "updated-refresh-token";
2764
2765    struct StaticVerifier {
2766        token: &'static str,
2767        kind: &'static str,
2768    }
2769
2770    impl ApplicationVerifier for StaticVerifier {
2771        fn verify(&self) -> AuthResult<String> {
2772            Ok(self.token.to_string())
2773        }
2774
2775        fn verifier_type(&self) -> &str {
2776            self.kind
2777        }
2778    }
2779
2780    fn build_auth(server: &MockServer) -> Arc<Auth> {
2781        Auth::builder(test_firebase_app_with_api_key(TEST_API_KEY))
2782            .with_identity_toolkit_endpoint(server.url("/v1"))
2783            .with_secure_token_endpoint(server.url("/token"))
2784            .defer_initialization()
2785            .build()
2786            .expect("failed to build auth")
2787    }
2788
2789    async fn sign_in_user(auth: &Arc<Auth>, server: &MockServer) {
2790        let mock = server.mock(|when, then| {
2791            when.method(POST)
2792                .path("/v1/accounts:signInWithPassword")
2793                .query_param("key", TEST_API_KEY)
2794                .json_body(json!({
2795                    "email": TEST_EMAIL,
2796                    "password": TEST_PASSWORD,
2797                    "returnSecureToken": true
2798                }));
2799            then.status(200).json_body(json!({
2800                "localId": TEST_UID,
2801                "email": TEST_EMAIL,
2802                "idToken": TEST_ID_TOKEN,
2803                "refreshToken": TEST_REFRESH_TOKEN,
2804                "expiresIn": "3600"
2805            }));
2806        });
2807
2808        auth.sign_in_with_email_and_password(TEST_EMAIL, TEST_PASSWORD)
2809            .await
2810            .expect("sign-in should succeed");
2811        mock.assert();
2812    }
2813
2814    #[tokio::test(flavor = "current_thread")]
2815    async fn sign_in_with_email_and_password_success() {
2816        let server = start_mock_server();
2817        let auth = build_auth(&server);
2818
2819        let mock = server.mock(|when, then| {
2820            when.method(POST)
2821                .path("/v1/accounts:signInWithPassword")
2822                .query_param("key", TEST_API_KEY)
2823                .json_body(json!({
2824                    "email": "user@example.com",
2825                    "password": "secret",
2826                    "returnSecureToken": true
2827                }));
2828            then.status(200).json_body(json!({
2829                "localId": "uid-123",
2830                "email": "user@example.com",
2831                "idToken": "id-token",
2832                "refreshToken": "refresh-token",
2833                "expiresIn": "3600"
2834            }));
2835        });
2836
2837        let credential = auth
2838            .sign_in_with_email_and_password("user@example.com", "secret")
2839            .await
2840            .expect("sign-in should succeed");
2841
2842        mock.assert();
2843        assert_eq!(
2844            credential.provider_id.as_deref(),
2845            Some(EmailAuthProvider::PROVIDER_ID)
2846        );
2847        assert_eq!(credential.operation_type.as_deref(), Some("signIn"));
2848        assert_eq!(credential.user.uid(), "uid-123");
2849        assert_eq!(
2850            credential.user.token_manager().access_token(),
2851            Some("id-token".to_string())
2852        );
2853        assert_eq!(
2854            credential.user.refresh_token(),
2855            Some("refresh-token".to_string())
2856        );
2857    }
2858
2859    #[tokio::test(flavor = "current_thread")]
2860    async fn multi_factor_sign_in_flow() {
2861        let server = start_mock_server();
2862        let auth = build_auth(&server);
2863
2864        let sign_in_mock = server.mock(|when, then| {
2865            when.method(POST)
2866                .path("/v1/accounts:signInWithPassword")
2867                .query_param("key", TEST_API_KEY)
2868                .json_body(json!({
2869                    "email": TEST_EMAIL,
2870                    "password": TEST_PASSWORD,
2871                    "returnSecureToken": true
2872                }));
2873            then.status(200).json_body(json!({
2874                "localId": TEST_UID,
2875                "email": TEST_EMAIL,
2876                "mfaPendingCredential": "PENDING_TOKEN",
2877                "mfaInfo": [{
2878                    "mfaEnrollmentId": "enroll1",
2879                    "displayName": "Work phone",
2880                    "phoneInfo": "+15558675309"
2881                }]
2882            }));
2883        });
2884
2885        let error = auth
2886            .sign_in_with_email_and_password(TEST_EMAIL, TEST_PASSWORD)
2887            .await
2888            .expect_err("expected multi-factor challenge");
2889
2890        sign_in_mock.assert();
2891
2892        let firebase_auth = FirebaseAuth::new(auth.clone());
2893        let resolver = get_multi_factor_resolver(&firebase_auth, &error)
2894            .expect("resolver should be constructed");
2895
2896        assert_eq!(resolver.hints().len(), 1);
2897        assert_eq!(resolver.hints()[0].uid, "enroll1");
2898        assert_eq!(
2899            resolver.session().pending_credential(),
2900            Some("PENDING_TOKEN")
2901        );
2902
2903        let verifier = Arc::new(StaticVerifier {
2904            token: "recaptcha-token",
2905            kind: "recaptcha",
2906        });
2907
2908        let start_mock = server.mock(|when, then| {
2909            when.method(POST)
2910                .path("/v1/accounts/mfaSignIn:start")
2911                .query_param("key", TEST_API_KEY)
2912                .json_body(json!({
2913                    "mfaPendingCredential": "PENDING_TOKEN",
2914                    "mfaEnrollmentId": "enroll1",
2915                    "phoneSignInInfo": {
2916                        "recaptchaToken": "recaptcha-token",
2917                        "clientType": CLIENT_TYPE_WEB
2918                    }
2919                }));
2920            then.status(200).json_body(json!({
2921                "phoneResponseInfo": { "sessionInfo": "SESSION_INFO" }
2922            }));
2923        });
2924
2925        let verification_id = resolver
2926            .send_phone_sign_in_code(&resolver.hints()[0], verifier)
2927            .await
2928            .expect("start MFA sign-in should succeed");
2929        start_mock.assert();
2930        assert_eq!(verification_id, "SESSION_INFO");
2931
2932        let finalize_mock = server.mock(|when, then| {
2933            when.method(POST)
2934                .path("/v1/accounts/mfaSignIn:finalize")
2935                .query_param("key", TEST_API_KEY)
2936                .json_body(json!({
2937                    "mfaPendingCredential": "PENDING_TOKEN",
2938                    "phoneVerificationInfo": {
2939                        "sessionInfo": "SESSION_INFO",
2940                        "code": "123456"
2941                    }
2942                }));
2943            then.status(200).json_body(json!({
2944                "idToken": "mfa-id-token",
2945                "refreshToken": "mfa-refresh-token"
2946            }));
2947        });
2948
2949        let credential = PhoneAuthProvider::credential(verification_id, "123456");
2950        let assertion = PhoneMultiFactorGenerator::assertion(credential);
2951        let result = resolver
2952            .resolve_sign_in(assertion)
2953            .await
2954            .expect("multi-factor sign-in should succeed");
2955
2956        finalize_mock.assert();
2957        assert_eq!(result.user.uid(), TEST_UID);
2958        assert_eq!(
2959            result.provider_id.as_deref(),
2960            Some(EmailAuthProvider::PROVIDER_ID)
2961        );
2962        assert_eq!(
2963            result.user.token_manager().access_token(),
2964            Some("mfa-id-token".to_string())
2965        );
2966        assert_eq!(
2967            result.user.token_manager().refresh_token(),
2968            Some("mfa-refresh-token".to_string())
2969        );
2970    }
2971
2972    #[tokio::test(flavor = "current_thread")]
2973    async fn multi_factor_reauthentication_flow() {
2974        let server = start_mock_server();
2975        let auth = build_auth(&server);
2976        sign_in_user(&auth, &server).await;
2977
2978        let reauth_mock = server.mock(|when, then| {
2979            when.method(POST)
2980                .path("/v1/accounts:signInWithPassword")
2981                .query_param("key", TEST_API_KEY)
2982                .json_body(json!({
2983                    "email": REAUTH_EMAIL,
2984                    "password": TEST_PASSWORD,
2985                    "returnSecureToken": true
2986                }));
2987            then.status(200).json_body(json!({
2988                "localId": TEST_UID,
2989                "email": REAUTH_EMAIL,
2990                "mfaPendingCredential": "REAUTH_PENDING",
2991                "mfaInfo": [{
2992                    "mfaEnrollmentId": "enroll1",
2993                    "displayName": "Work phone",
2994                    "phoneInfo": "+15558675309"
2995                }]
2996            }));
2997        });
2998
2999        let error = auth
3000            .reauthenticate_with_password(REAUTH_EMAIL, TEST_PASSWORD)
3001            .await
3002            .expect_err("expected multi-factor challenge during reauth");
3003
3004        reauth_mock.assert();
3005
3006        let firebase_auth = FirebaseAuth::new(auth.clone());
3007        let resolver = get_multi_factor_resolver(&firebase_auth, &error)
3008            .expect("resolver should be constructed");
3009
3010        assert_eq!(resolver.hints().len(), 1);
3011        assert_eq!(resolver.hints()[0].uid, "enroll1");
3012        assert_eq!(
3013            resolver.session().pending_credential(),
3014            Some("REAUTH_PENDING")
3015        );
3016
3017        let verifier = Arc::new(StaticVerifier {
3018            token: "recaptcha-token",
3019            kind: "recaptcha",
3020        });
3021
3022        let start_mock = server.mock(|when, then| {
3023            when.method(POST)
3024                .path("/v1/accounts/mfaSignIn:start")
3025                .query_param("key", TEST_API_KEY)
3026                .json_body(json!({
3027                    "mfaPendingCredential": "REAUTH_PENDING",
3028                    "mfaEnrollmentId": "enroll1",
3029                    "phoneSignInInfo": {
3030                        "recaptchaToken": "recaptcha-token",
3031                        "clientType": CLIENT_TYPE_WEB
3032                    }
3033                }));
3034            then.status(200).json_body(json!({
3035                "phoneResponseInfo": { "sessionInfo": "SESSION_INFO" }
3036            }));
3037        });
3038
3039        let verification_id = resolver
3040            .send_phone_sign_in_code(&resolver.hints()[0], verifier)
3041            .await
3042            .expect("start MFA reauth should succeed");
3043        start_mock.assert();
3044        assert_eq!(verification_id, "SESSION_INFO");
3045
3046        let finalize_mock = server.mock(|when, then| {
3047            when.method(POST)
3048                .path("/v1/accounts/mfaSignIn:finalize")
3049                .query_param("key", TEST_API_KEY)
3050                .json_body(json!({
3051                    "mfaPendingCredential": "REAUTH_PENDING",
3052                    "phoneVerificationInfo": {
3053                        "sessionInfo": "SESSION_INFO",
3054                        "code": "123456"
3055                    }
3056                }));
3057            then.status(200).json_body(json!({
3058                "idToken": "reauth-mfa-id-token",
3059                "refreshToken": "reauth-mfa-refresh-token"
3060            }));
3061        });
3062
3063        let credential = PhoneAuthProvider::credential(verification_id, "123456");
3064        let assertion = PhoneMultiFactorGenerator::assertion(credential);
3065        let result = resolver
3066            .resolve_sign_in(assertion)
3067            .await
3068            .expect("multi-factor reauth should succeed");
3069
3070        finalize_mock.assert();
3071        assert_eq!(result.operation_type.as_deref(), Some("reauthenticate"));
3072        assert_eq!(
3073            result.provider_id.as_deref(),
3074            Some(EmailAuthProvider::PROVIDER_ID)
3075        );
3076        assert_eq!(
3077            result.user.token_manager().access_token(),
3078            Some("reauth-mfa-id-token".to_string())
3079        );
3080        assert_eq!(
3081            result.user.token_manager().refresh_token(),
3082            Some("reauth-mfa-refresh-token".to_string())
3083        );
3084    }
3085
3086    #[tokio::test(flavor = "current_thread")]
3087    async fn multi_factor_link_flow() {
3088        let server = start_mock_server();
3089        let auth = build_auth(&server);
3090        sign_in_user(&auth, &server).await;
3091
3092        let link_mock = server.mock(|when, then| {
3093            when.method(POST)
3094                .path("/v1/accounts:signInWithPhoneNumber")
3095                .query_param("key", TEST_API_KEY)
3096                .json_body(json!({
3097                    "sessionInfo": "LINK_SESSION",
3098                    "code": "000000",
3099                    "idToken": TEST_ID_TOKEN
3100                }));
3101            then.status(200).json_body(json!({
3102                "localId": TEST_UID,
3103                "phoneNumber": "+15551234567",
3104                "mfaPendingCredential": "LINK_PENDING",
3105                "mfaInfo": [{
3106                    "mfaEnrollmentId": "enroll1",
3107                    "displayName": "Work phone",
3108                    "phoneInfo": "+15558675309"
3109                }]
3110            }));
3111        });
3112
3113        let initial_credential = PhoneAuthProvider::credential("LINK_SESSION", "000000");
3114        let error = auth
3115            .link_with_phone_credential(initial_credential)
3116            .await
3117            .expect_err("expected multi-factor requirement during link");
3118
3119        link_mock.assert();
3120
3121        let firebase_auth = FirebaseAuth::new(auth.clone());
3122        let resolver = get_multi_factor_resolver(&firebase_auth, &error)
3123            .expect("resolver should be constructed");
3124
3125        assert_eq!(resolver.hints().len(), 1);
3126        assert_eq!(resolver.hints()[0].uid, "enroll1");
3127        assert_eq!(
3128            resolver.session().pending_credential(),
3129            Some("LINK_PENDING")
3130        );
3131
3132        let verifier = Arc::new(StaticVerifier {
3133            token: "recaptcha-token",
3134            kind: "recaptcha",
3135        });
3136
3137        let start_mock = server.mock(|when, then| {
3138            when.method(POST)
3139                .path("/v1/accounts/mfaSignIn:start")
3140                .query_param("key", TEST_API_KEY)
3141                .json_body(json!({
3142                    "mfaPendingCredential": "LINK_PENDING",
3143                    "mfaEnrollmentId": "enroll1",
3144                    "phoneSignInInfo": {
3145                        "recaptchaToken": "recaptcha-token",
3146                        "clientType": CLIENT_TYPE_WEB
3147                    }
3148                }));
3149            then.status(200).json_body(json!({
3150                "phoneResponseInfo": { "sessionInfo": "SESSION_INFO" }
3151            }));
3152        });
3153
3154        let verification_id = resolver
3155            .send_phone_sign_in_code(&resolver.hints()[0], verifier)
3156            .await
3157            .expect("start MFA link should succeed");
3158        start_mock.assert();
3159        assert_eq!(verification_id, "SESSION_INFO");
3160
3161        let finalize_mock = server.mock(|when, then| {
3162            when.method(POST)
3163                .path("/v1/accounts/mfaSignIn:finalize")
3164                .query_param("key", TEST_API_KEY)
3165                .json_body(json!({
3166                    "mfaPendingCredential": "LINK_PENDING",
3167                    "phoneVerificationInfo": {
3168                        "sessionInfo": "SESSION_INFO",
3169                        "code": "123456"
3170                    }
3171                }));
3172            then.status(200).json_body(json!({
3173                "idToken": "link-mfa-id-token",
3174                "refreshToken": "link-mfa-refresh-token"
3175            }));
3176        });
3177
3178        let credential = PhoneAuthProvider::credential(verification_id, "123456");
3179        let assertion = PhoneMultiFactorGenerator::assertion(credential);
3180        let result = resolver
3181            .resolve_sign_in(assertion)
3182            .await
3183            .expect("multi-factor link should succeed");
3184
3185        finalize_mock.assert();
3186        assert_eq!(result.operation_type.as_deref(), Some("link"));
3187        assert_eq!(result.provider_id.as_deref(), Some(PHONE_PROVIDER_ID));
3188        assert_eq!(
3189            result.user.token_manager().access_token(),
3190            Some("link-mfa-id-token".to_string())
3191        );
3192        assert_eq!(
3193            result.user.token_manager().refresh_token(),
3194            Some("link-mfa-refresh-token".to_string())
3195        );
3196        assert_eq!(
3197            result.user.info().phone_number.as_deref(),
3198            Some("+15551234567")
3199        );
3200    }
3201
3202    #[tokio::test(flavor = "current_thread")]
3203    async fn passkey_multi_factor_link_flow() {
3204        let server = start_mock_server();
3205        let auth = build_auth(&server);
3206        sign_in_user(&auth, &server).await;
3207
3208        let link_mock = server.mock(|when, then| {
3209            when.method(POST)
3210                .path("/v1/accounts:signInWithPhoneNumber")
3211                .query_param("key", TEST_API_KEY)
3212                .json_body(json!({
3213                    "sessionInfo": "LINK_SESSION",
3214                    "code": "000000",
3215                    "idToken": TEST_ID_TOKEN
3216                }));
3217            then.status(200).json_body(json!({
3218                "localId": TEST_UID,
3219                "email": TEST_EMAIL,
3220                "mfaPendingCredential": "LINK_PENDING",
3221                "mfaInfo": [{
3222                    "mfaEnrollmentId": "passkey-enroll",
3223                    "displayName": "Security Key",
3224                    "factorId": "webauthn",
3225                    "webauthnInfo": { "displayName": "Security Key" },
3226                    "enrolledAt": "2024-02-01T10:00:00Z"
3227                }]
3228            }));
3229        });
3230
3231        let initial_credential = PhoneAuthProvider::credential("LINK_SESSION", "000000");
3232        let error = auth
3233            .link_with_phone_credential(initial_credential)
3234            .await
3235            .expect_err("expected multi-factor requirement during passkey link");
3236
3237        link_mock.assert();
3238
3239        let firebase_auth = FirebaseAuth::new(auth.clone());
3240        let resolver = get_multi_factor_resolver(&firebase_auth, &error)
3241            .expect("resolver should be constructed");
3242
3243        assert_eq!(resolver.hints().len(), 1);
3244        assert_eq!(resolver.hints()[0].uid, "passkey-enroll");
3245        assert_eq!(
3246            resolver.session().pending_credential(),
3247            Some("LINK_PENDING")
3248        );
3249
3250        let start_mock = server.mock(|when, then| {
3251            when.method(POST)
3252                .path("/v1/accounts/mfaSignIn:start")
3253                .query_param("key", TEST_API_KEY)
3254                .json_body(json!({
3255                    "mfaPendingCredential": "LINK_PENDING",
3256                    "mfaEnrollmentId": "passkey-enroll"
3257                }));
3258            then.status(200).json_body(json!({
3259                "webauthnSignInInfo": {
3260                    "challenge": "LINK_PASSKEY_CHALLENGE",
3261                    "rpId": "example.com"
3262                }
3263            }));
3264        });
3265
3266        let challenge = resolver
3267            .start_passkey_sign_in(&resolver.hints()[0])
3268            .await
3269            .expect("passkey challenge should succeed");
3270        start_mock.assert();
3271        assert_eq!(challenge.challenge(), Some("LINK_PASSKEY_CHALLENGE"));
3272
3273        let verification_payload = json!({
3274            "credentialId": "cred-123",
3275            "clientDataJSON": "CLIENT",
3276            "authenticatorData": "AUTH",
3277            "signature": "SIG"
3278        });
3279
3280        let finalize_mock = server.mock(|when, then| {
3281            when.method(POST)
3282                .path("/v1/accounts/mfaSignIn:finalize")
3283                .query_param("key", TEST_API_KEY)
3284                .json_body(json!({
3285                    "mfaPendingCredential": "LINK_PENDING",
3286                    "mfaEnrollmentId": "passkey-enroll",
3287                    "webauthnVerificationInfo": verification_payload
3288                }));
3289            then.status(200).json_body(json!({
3290                "idToken": "link-passkey-id-token",
3291                "refreshToken": "link-passkey-refresh-token"
3292            }));
3293        });
3294
3295        let assertion_response = WebAuthnAssertionResponse::try_from(json!({
3296            "credentialId": "cred-123",
3297            "clientDataJSON": "CLIENT",
3298            "authenticatorData": "AUTH",
3299            "signature": "SIG"
3300        }))
3301        .expect("verification payload should be valid");
3302
3303        let assertion = WebAuthnMultiFactorGenerator::assertion_for_sign_in(
3304            "passkey-enroll",
3305            assertion_response,
3306        );
3307
3308        let result = resolver
3309            .resolve_sign_in(assertion)
3310            .await
3311            .expect("passkey link should succeed");
3312
3313        finalize_mock.assert();
3314        assert_eq!(result.operation_type.as_deref(), Some("link"));
3315        assert_eq!(result.provider_id.as_deref(), Some(PHONE_PROVIDER_ID));
3316        assert_eq!(
3317            result.user.token_manager().access_token(),
3318            Some("link-passkey-id-token".to_string())
3319        );
3320        assert_eq!(
3321            result.user.token_manager().refresh_token(),
3322            Some("link-passkey-refresh-token".to_string())
3323        );
3324    }
3325
3326    #[tokio::test(flavor = "current_thread")]
3327    async fn passkey_multi_factor_sign_in_flow() {
3328        let server = start_mock_server();
3329        let auth = build_auth(&server);
3330
3331        let sign_in_mock = server.mock(|when, then| {
3332            when.method(POST)
3333                .path("/v1/accounts:signInWithPassword")
3334                .query_param("key", TEST_API_KEY)
3335                .json_body(json!({
3336                    "email": TEST_EMAIL,
3337                    "password": TEST_PASSWORD,
3338                    "returnSecureToken": true
3339                }));
3340            then.status(200).json_body(json!({
3341                "localId": TEST_UID,
3342                "email": TEST_EMAIL,
3343                "mfaPendingCredential": "PASSKEY_PENDING",
3344                "mfaInfo": [{
3345                    "mfaEnrollmentId": "enroll1",
3346                    "displayName": "Security Key",
3347                    "factorId": "webauthn"
3348                }]
3349            }));
3350        });
3351
3352        let error = auth
3353            .sign_in_with_email_and_password(TEST_EMAIL, TEST_PASSWORD)
3354            .await
3355            .expect_err("expected multi-factor challenge");
3356
3357        sign_in_mock.assert();
3358
3359        let firebase_auth = FirebaseAuth::new(auth.clone());
3360        let resolver = get_multi_factor_resolver(&firebase_auth, &error)
3361            .expect("resolver should be constructed");
3362
3363        let start_mock = server.mock(|when, then| {
3364            when.method(POST)
3365                .path("/v1/accounts/mfaSignIn:start")
3366                .query_param("key", TEST_API_KEY)
3367                .json_body(json!({
3368                    "mfaPendingCredential": "PASSKEY_PENDING",
3369                    "mfaEnrollmentId": "enroll1"
3370                }));
3371            then.status(200).json_body(json!({
3372                "webauthnSignInInfo": {
3373                    "challenge": "PASSKEY_CHALLENGE",
3374                    "rpId": "example.com"
3375                }
3376            }));
3377        });
3378
3379        let challenge = resolver
3380            .start_passkey_sign_in(&resolver.hints()[0])
3381            .await
3382            .expect("passkey challenge should succeed");
3383        start_mock.assert();
3384        assert_eq!(challenge.challenge(), Some("PASSKEY_CHALLENGE"));
3385
3386        let verification_info = json!({
3387            "credentialId": "cred-123",
3388            "clientDataJSON": "BASE64CLIENT",
3389            "authenticatorData": "BASE64DATA",
3390            "signature": "BASE64SIG"
3391        });
3392
3393        let verification_info_clone = verification_info.clone();
3394        let finalize_mock = server.mock(|when, then| {
3395            when.method(POST)
3396                .path("/v1/accounts/mfaSignIn:finalize")
3397                .query_param("key", TEST_API_KEY)
3398                .json_body(json!({
3399                    "mfaPendingCredential": "PASSKEY_PENDING",
3400                    "mfaEnrollmentId": "enroll1",
3401                    "webauthnVerificationInfo": verification_info_clone
3402                }));
3403            then.status(200).json_body(json!({
3404                "idToken": "passkey-id-token",
3405                "refreshToken": "passkey-refresh-token"
3406            }));
3407        });
3408
3409        let assertion_response = WebAuthnAssertionResponse::try_from(verification_info)
3410            .expect("verification payload should be valid");
3411        let assertion =
3412            WebAuthnMultiFactorGenerator::assertion_for_sign_in("enroll1", assertion_response);
3413        let result = resolver
3414            .resolve_sign_in(assertion)
3415            .await
3416            .expect("passkey multi-factor sign-in should succeed");
3417
3418        finalize_mock.assert();
3419        assert_eq!(result.user.uid(), TEST_UID);
3420        assert_eq!(
3421            result.provider_id.as_deref(),
3422            Some(EmailAuthProvider::PROVIDER_ID)
3423        );
3424        assert_eq!(
3425            result.user.token_manager().access_token(),
3426            Some("passkey-id-token".to_string())
3427        );
3428        assert_eq!(
3429            result.user.token_manager().refresh_token(),
3430            Some("passkey-refresh-token".to_string())
3431        );
3432    }
3433
3434    #[tokio::test(flavor = "current_thread")]
3435    async fn multi_factor_hints_sorted_by_enrollment_time() {
3436        let server = start_mock_server();
3437        let auth = build_auth(&server);
3438
3439        let sign_in_mock = server.mock(|when, then| {
3440            when.method(POST)
3441                .path("/v1/accounts:signInWithPassword")
3442                .query_param("key", TEST_API_KEY)
3443                .json_body(json!({
3444                    "email": TEST_EMAIL,
3445                    "password": TEST_PASSWORD,
3446                    "returnSecureToken": true
3447                }));
3448            then.status(200).json_body(json!({
3449                "localId": TEST_UID,
3450                "email": TEST_EMAIL,
3451                "mfaPendingCredential": "PENDING_SORT",
3452                "mfaInfo": [
3453                    {
3454                        "mfaEnrollmentId": "second",
3455                        "displayName": "Security Key",
3456                        "factorId": "webauthn",
3457                        "webauthnInfo": { "displayName": "Security Key" },
3458                        "enrolledAt": "2024-06-05T10:00:00Z"
3459                    },
3460                    {
3461                        "mfaEnrollmentId": "first",
3462                        "displayName": "Phone",
3463                        "phoneInfo": "+15551234567",
3464                        "enrolledAt": "2023-01-01T00:00:00Z"
3465                    }
3466                ]
3467            }));
3468        });
3469
3470        let error = auth
3471            .sign_in_with_email_and_password(TEST_EMAIL, TEST_PASSWORD)
3472            .await
3473            .expect_err("expected multi-factor challenge");
3474
3475        sign_in_mock.assert();
3476
3477        let firebase_auth = FirebaseAuth::new(auth.clone());
3478        let resolver = get_multi_factor_resolver(&firebase_auth, &error)
3479            .expect("resolver should be constructed");
3480
3481        assert_eq!(resolver.hints().len(), 2);
3482        assert_eq!(resolver.hints()[0].uid, "first");
3483        assert_eq!(resolver.hints()[0].display_name.as_deref(), Some("Phone"));
3484        assert_eq!(resolver.hints()[1].uid, "second");
3485        assert_eq!(
3486            resolver.hints()[1].display_name.as_deref(),
3487            Some("Security Key")
3488        );
3489    }
3490
3491    #[tokio::test(flavor = "current_thread")]
3492    async fn passkey_sign_in_invalid_challenge_error() {
3493        let server = start_mock_server();
3494        let auth = build_auth(&server);
3495
3496        let sign_in_mock = server.mock(|when, then| {
3497            when.method(POST)
3498                .path("/v1/accounts:signInWithPassword")
3499                .query_param("key", TEST_API_KEY)
3500                .json_body(json!({
3501                    "email": TEST_EMAIL,
3502                    "password": TEST_PASSWORD,
3503                    "returnSecureToken": true
3504                }));
3505            then.status(200).json_body(json!({
3506                "localId": TEST_UID,
3507                "email": TEST_EMAIL,
3508                "mfaPendingCredential": "PASSKEY_PENDING",
3509                "mfaInfo": [{
3510                    "mfaEnrollmentId": "enroll1",
3511                    "displayName": "Security Key",
3512                    "factorId": "webauthn"
3513                }]
3514            }));
3515        });
3516
3517        let error = auth
3518            .sign_in_with_email_and_password(TEST_EMAIL, TEST_PASSWORD)
3519            .await
3520            .expect_err("expected multi-factor challenge");
3521
3522        sign_in_mock.assert();
3523
3524        let firebase_auth = FirebaseAuth::new(auth.clone());
3525        let resolver = get_multi_factor_resolver(&firebase_auth, &error)
3526            .expect("resolver should be constructed");
3527
3528        let start_mock = server.mock(|when, then| {
3529            when.method(POST)
3530                .path("/v1/accounts/mfaSignIn:start")
3531                .query_param("key", TEST_API_KEY)
3532                .json_body(json!({
3533                    "mfaPendingCredential": "PASSKEY_PENDING",
3534                    "mfaEnrollmentId": "enroll1"
3535                }));
3536            then.status(400).json_body(json!({
3537                "error": {
3538                    "code": 400,
3539                    "message": "INVALID_CHALLENGE",
3540                    "errors": [{
3541                        "message": "INVALID_CHALLENGE"
3542                    }]
3543                }
3544            }));
3545        });
3546
3547        let err = resolver
3548            .start_passkey_sign_in(&resolver.hints()[0])
3549            .await
3550            .expect_err("invalid challenge should propagate as error");
3551
3552        start_mock.assert();
3553
3554        match err {
3555            AuthError::InvalidCredential(message) => {
3556                assert!(message.contains("INVALID_CHALLENGE"));
3557            }
3558            _ => panic!("unexpected error variant: {err:?}"),
3559        }
3560    }
3561
3562    #[tokio::test(flavor = "current_thread")]
3563    async fn passkey_multi_factor_enrollment_flow() {
3564        let server = start_mock_server();
3565        let auth = build_auth(&server);
3566        sign_in_user(&auth, &server).await;
3567
3568        let multi_factor = auth.multi_factor();
3569        let session = multi_factor
3570            .get_session()
3571            .await
3572            .expect("session should be created");
3573
3574        let start_mock = server.mock(|when, then| {
3575            when.method(POST)
3576                .path("/v1/accounts/mfaEnrollment:start")
3577                .query_param("key", TEST_API_KEY)
3578                .json_body(json!({
3579                    "idToken": TEST_ID_TOKEN,
3580                    "webauthnEnrollmentInfo": {}
3581                }));
3582            then.status(200).json_body(json!({
3583                "webauthnEnrollmentInfo": {
3584                    "challenge": "ENROLL_CHALLENGE",
3585                    "rpId": "example.com",
3586                    "user": { "name": "alice@example.com" }
3587                }
3588            }));
3589        });
3590
3591        let challenge = multi_factor
3592            .start_passkey_enrollment(&session)
3593            .await
3594            .expect("passkey challenge should succeed");
3595        start_mock.assert();
3596        assert_eq!(challenge.challenge(), Some("ENROLL_CHALLENGE"));
3597
3598        let attestation_value = json!({
3599            "credentialId": "cred-123",
3600            "clientDataJSON": "BASE64CLIENT",
3601            "attestationObject": "BASE64ATTEST"
3602        });
3603
3604        let lookup_mock = server.mock(|when, then| {
3605            when.method(POST)
3606                .path("/v1/accounts:lookup")
3607                .query_param("key", TEST_API_KEY)
3608                .json_body(json!({ "idToken": "passkey-id-token" }));
3609            then.status(200).json_body(json!({
3610                "users": [{
3611                    "localId": TEST_UID,
3612                    "email": TEST_EMAIL,
3613                    "mfaInfo": [{
3614                        "mfaEnrollmentId": "enroll1",
3615                        "displayName": "Security Key",
3616                        "factorId": "webauthn",
3617                        "webauthnInfo": { "displayName": "Security Key" }
3618                    }]
3619                }]
3620            }));
3621        });
3622
3623        let finalize_mock = server.mock(|when, then| {
3624            when.method(POST)
3625                .path("/v1/accounts/mfaEnrollment:finalize")
3626                .query_param("key", TEST_API_KEY)
3627                .json_body(json!({
3628                    "idToken": TEST_ID_TOKEN,
3629                    "webauthnVerificationInfo": attestation_value,
3630                    "displayName": "Security Key"
3631                }));
3632            then.status(200).json_body(json!({
3633                "idToken": "passkey-id-token",
3634                "refreshToken": "passkey-refresh-token"
3635            }));
3636        });
3637
3638        let attestation = WebAuthnAttestationResponse::try_from(attestation_value.clone())
3639            .expect("attestation payload should be valid");
3640        let assertion = WebAuthnMultiFactorGenerator::assertion_for_enrollment(attestation);
3641        let result = multi_factor
3642            .enroll(&session, assertion, Some("Security Key"))
3643            .await
3644            .expect("passkey enrollment should succeed");
3645
3646        start_mock.assert();
3647        finalize_mock.assert();
3648        lookup_mock.assert();
3649        assert_eq!(result.provider_id.as_deref(), Some(WEBAUTHN_FACTOR_ID));
3650        assert_eq!(
3651            result.user.token_manager().access_token(),
3652            Some("passkey-id-token".to_string())
3653        );
3654        assert_eq!(
3655            result.user.token_manager().refresh_token(),
3656            Some("passkey-refresh-token".to_string())
3657        );
3658    }
3659
3660    #[tokio::test(flavor = "current_thread")]
3661    async fn passkey_enrollment_missing_verification_error() {
3662        let server = start_mock_server();
3663        let auth = build_auth(&server);
3664        sign_in_user(&auth, &server).await;
3665
3666        let multi_factor = auth.multi_factor();
3667        let session = multi_factor
3668            .get_session()
3669            .await
3670            .expect("session should be created");
3671
3672        let start_mock = server.mock(|when, then| {
3673            when.method(POST)
3674                .path("/v1/accounts/mfaEnrollment:start")
3675                .query_param("key", TEST_API_KEY)
3676                .json_body(json!({
3677                    "idToken": TEST_ID_TOKEN,
3678                    "webauthnEnrollmentInfo": {}
3679                }));
3680            then.status(200).json_body(json!({
3681                "webauthnEnrollmentInfo": {
3682                    "challenge": "ENROLL_CHALLENGE",
3683                    "rpId": "example.com",
3684                    "user": { "name": "alice@example.com" }
3685                }
3686            }));
3687        });
3688
3689        let challenge = multi_factor
3690            .start_passkey_enrollment(&session)
3691            .await
3692            .expect("challenge should succeed");
3693        start_mock.assert();
3694        assert_eq!(challenge.challenge(), Some("ENROLL_CHALLENGE"));
3695
3696        let finalize_mock = server.mock(|when, then| {
3697            when.method(POST)
3698                .path("/v1/accounts/mfaEnrollment:finalize")
3699                .query_param("key", TEST_API_KEY)
3700                .json_body(json!({
3701                    "idToken": TEST_ID_TOKEN,
3702                    "webauthnVerificationInfo": {
3703                        "credentialId": "cred-123",
3704                        "clientDataJSON": "BASE64CLIENT",
3705                        "attestationObject": "BASE64ATTEST"
3706                    },
3707                    "displayName": "Security Key"
3708                }));
3709            then.status(400).json_body(json!({
3710                "error": {
3711                    "code": 400,
3712                    "message": "MISSING_WEBAUTHN_VERIFICATION_INFO",
3713                    "errors": [{
3714                        "message": "MISSING_WEBAUTHN_VERIFICATION_INFO"
3715                    }]
3716                }
3717            }));
3718        });
3719
3720        let attestation = WebAuthnAttestationResponse::try_from(json!({
3721            "credentialId": "cred-123",
3722            "clientDataJSON": "BASE64CLIENT",
3723            "attestationObject": "BASE64ATTEST"
3724        }))
3725        .expect("attestation should parse");
3726
3727        let assertion = WebAuthnMultiFactorGenerator::assertion_for_enrollment(attestation);
3728
3729        let err = multi_factor
3730            .enroll(&session, assertion, Some("Security Key"))
3731            .await
3732            .expect_err("missing verification info should error");
3733
3734        finalize_mock.assert();
3735
3736        match err {
3737            AuthError::InvalidCredential(message) => {
3738                assert!(message.contains("MISSING_WEBAUTHN_VERIFICATION_INFO"));
3739            }
3740            _ => panic!("unexpected error variant: {err:?}"),
3741        }
3742    }
3743
3744    #[tokio::test(flavor = "current_thread")]
3745    async fn passkey_sign_in_missing_mfa_info_error() {
3746        let server = start_mock_server();
3747        let auth = build_auth(&server);
3748
3749        let sign_in_mock = server.mock(|when, then| {
3750            when.method(POST)
3751                .path("/v1/accounts:signInWithPassword")
3752                .query_param("key", TEST_API_KEY)
3753                .json_body(json!({
3754                    "email": TEST_EMAIL,
3755                    "password": TEST_PASSWORD,
3756                    "returnSecureToken": true
3757                }));
3758            then.status(200).json_body(json!({
3759                "localId": TEST_UID,
3760                "email": TEST_EMAIL,
3761                "mfaPendingCredential": "PASSKEY_PENDING",
3762                "mfaInfo": [{
3763                    "mfaEnrollmentId": "enroll1",
3764                    "displayName": "Security Key",
3765                    "factorId": "webauthn"
3766                }]
3767            }));
3768        });
3769
3770        let error = auth
3771            .sign_in_with_email_and_password(TEST_EMAIL, TEST_PASSWORD)
3772            .await
3773            .expect_err("expected multi-factor challenge");
3774
3775        sign_in_mock.assert();
3776
3777        let firebase_auth = FirebaseAuth::new(auth.clone());
3778        let resolver = get_multi_factor_resolver(&firebase_auth, &error)
3779            .expect("resolver should be constructed");
3780
3781        let start_mock = server.mock(|when, then| {
3782            when.method(POST)
3783                .path("/v1/accounts/mfaSignIn:start")
3784                .query_param("key", TEST_API_KEY)
3785                .json_body(json!({
3786                    "mfaPendingCredential": "PASSKEY_PENDING",
3787                    "mfaEnrollmentId": "enroll1"
3788                }));
3789            then.status(200).json_body(json!({
3790                "webauthnSignInInfo": {
3791                    "challenge": "PASSKEY_CHALLENGE",
3792                    "rpId": "example.com"
3793                }
3794            }));
3795        });
3796
3797        let challenge = resolver
3798            .start_passkey_sign_in(&resolver.hints()[0])
3799            .await
3800            .expect("challenge should succeed");
3801        start_mock.assert();
3802        assert_eq!(challenge.challenge(), Some("PASSKEY_CHALLENGE"));
3803
3804        let finalize_mock = server.mock(|when, then| {
3805            when.method(POST)
3806                .path("/v1/accounts/mfaSignIn:finalize")
3807                .query_param("key", TEST_API_KEY)
3808                .json_body(json!({
3809                    "mfaPendingCredential": "PASSKEY_PENDING",
3810                    "mfaEnrollmentId": "enroll1",
3811                    "webauthnVerificationInfo": {
3812                        "credentialId": "cred-123",
3813                        "clientDataJSON": "CLIENT",
3814                        "authenticatorData": "AUTH",
3815                        "signature": "SIG"
3816                    }
3817                }));
3818            then.status(400).json_body(json!({
3819                "error": {
3820                    "code": 400,
3821                    "message": "MISSING_MULTI_FACTOR_INFO",
3822                    "errors": [{
3823                        "message": "MISSING_MULTI_FACTOR_INFO"
3824                    }]
3825                }
3826            }));
3827        });
3828
3829        let response = WebAuthnAssertionResponse::try_from(json!({
3830            "credentialId": "cred-123",
3831            "clientDataJSON": "CLIENT",
3832            "authenticatorData": "AUTH",
3833            "signature": "SIG"
3834        }))
3835        .expect("assertion payload should parse");
3836
3837        let assertion = WebAuthnMultiFactorGenerator::assertion_for_sign_in("enroll1", response);
3838
3839        let err = resolver
3840            .resolve_sign_in(assertion)
3841            .await
3842            .expect_err("missing MFA info should error");
3843
3844        finalize_mock.assert();
3845
3846        match err {
3847            AuthError::MultiFactor(mfa_err) => {
3848                assert_eq!(mfa_err.code(), MultiFactorAuthErrorCode::MissingInfo);
3849                assert_eq!(mfa_err.server_message(), Some("MISSING_MULTI_FACTOR_INFO"));
3850            }
3851            other => panic!("unexpected error variant: {other:?}"),
3852        }
3853    }
3854
3855    #[tokio::test(flavor = "current_thread")]
3856    async fn totp_enrollment_flow() {
3857        let server = start_mock_server();
3858        let auth = build_auth(&server);
3859        sign_in_user(&auth, &server).await;
3860
3861        let multi_factor = auth.multi_factor();
3862        let session = multi_factor
3863            .get_session()
3864            .await
3865            .expect("session should be created");
3866
3867        let start_mock = server.mock(|when, then| {
3868            when.method(POST)
3869                .path("/v1/accounts/mfaEnrollment:start")
3870                .query_param("key", TEST_API_KEY)
3871                .json_body(json!({
3872                    "idToken": TEST_ID_TOKEN,
3873                    "totpEnrollmentInfo": {}
3874                }));
3875            then.status(200).json_body(json!({
3876                "totpSessionInfo": {
3877                    "sharedSecretKey": "SECRETKEY",
3878                    "verificationCodeLength": 6,
3879                    "hashingAlgorithm": "SHA1",
3880                    "periodSec": 30,
3881                    "sessionInfo": "TOTP_SESSION",
3882                    "finalizeEnrollmentTime": 1_000
3883                }
3884            }));
3885        });
3886
3887        let secret = multi_factor
3888            .generate_totp_secret(&session)
3889            .await
3890            .expect("secret generation should succeed");
3891        start_mock.assert();
3892        assert_eq!(secret.secret_key(), "SECRETKEY");
3893        assert_eq!(secret.code_length(), 6);
3894
3895        let qr_url = secret.qr_code_url(None, None);
3896        assert!(qr_url.contains("secret=SECRETKEY"));
3897
3898        let lookup_mock = server.mock(|when, then| {
3899            when.method(POST)
3900                .path("/v1/accounts:lookup")
3901                .query_param("key", TEST_API_KEY)
3902                .json_body(json!({ "idToken": "totp-id-token" }));
3903            then.status(200).json_body(json!({
3904                "users": [{
3905                    "localId": TEST_UID,
3906                    "email": TEST_EMAIL,
3907                    "mfaInfo": [{
3908                        "mfaEnrollmentId": "totp1",
3909                        "displayName": "Authenticator",
3910                        "factorId": "totp"
3911                    }]
3912                }]
3913            }));
3914        });
3915
3916        let finalize_mock = server.mock(|when, then| {
3917            when.method(POST)
3918                .path("/v1/accounts/mfaEnrollment:finalize")
3919                .query_param("key", TEST_API_KEY)
3920                .json_body(json!({
3921                    "idToken": TEST_ID_TOKEN,
3922                    "totpVerificationInfo": {
3923                        "sessionInfo": "TOTP_SESSION",
3924                        "verificationCode": "654321"
3925                    },
3926                    "displayName": "Authenticator"
3927                }));
3928            then.status(200).json_body(json!({
3929                "idToken": "totp-id-token",
3930                "refreshToken": "totp-refresh-token"
3931            }));
3932        });
3933
3934        let assertion =
3935            TotpMultiFactorGenerator::assertion_for_enrollment(secret.clone(), "654321");
3936        let result = multi_factor
3937            .enroll(&session, assertion, Some("Authenticator"))
3938            .await
3939            .expect("TOTP enrollment should succeed");
3940
3941        finalize_mock.assert();
3942        lookup_mock.assert();
3943        assert_eq!(result.provider_id.as_deref(), Some("totp"));
3944        assert_eq!(result.operation_type.as_deref(), Some("enroll"));
3945        assert_eq!(
3946            result.user.token_manager().access_token(),
3947            Some("totp-id-token".to_string())
3948        );
3949    }
3950
3951    #[tokio::test(flavor = "current_thread")]
3952    async fn totp_multi_factor_sign_in_flow() {
3953        let server = start_mock_server();
3954        let auth = build_auth(&server);
3955
3956        let sign_in_mock = server.mock(|when, then| {
3957            when.method(POST)
3958                .path("/v1/accounts:signInWithPassword")
3959                .query_param("key", TEST_API_KEY)
3960                .json_body(json!({
3961                    "email": TEST_EMAIL,
3962                    "password": TEST_PASSWORD,
3963                    "returnSecureToken": true
3964                }));
3965            then.status(200).json_body(json!({
3966                "localId": TEST_UID,
3967                "email": TEST_EMAIL,
3968                "mfaPendingCredential": "PENDING_TOTP",
3969                "mfaInfo": [{
3970                    "mfaEnrollmentId": "totp1",
3971                    "displayName": "Authenticator",
3972                    "factorId": "totp",
3973                    "totpInfo": {}
3974                }]
3975            }));
3976        });
3977
3978        let error = auth
3979            .sign_in_with_email_and_password(TEST_EMAIL, TEST_PASSWORD)
3980            .await
3981            .expect_err("expected multi-factor challenge");
3982        sign_in_mock.assert();
3983
3984        let firebase_auth = FirebaseAuth::new(auth.clone());
3985        let resolver = get_multi_factor_resolver(&firebase_auth, &error)
3986            .expect("resolver should be constructed");
3987        assert_eq!(resolver.hints()[0].factor_id, "totp");
3988
3989        let finalize_mock = server.mock(|when, then| {
3990            when.method(POST)
3991                .path("/v1/accounts/mfaSignIn:finalize")
3992                .query_param("key", TEST_API_KEY)
3993                .json_body(json!({
3994                    "mfaPendingCredential": "PENDING_TOTP",
3995                    "mfaEnrollmentId": "totp1",
3996                    "totpVerificationInfo": {
3997                        "verificationCode": "123456"
3998                    }
3999                }));
4000            then.status(200).json_body(json!({
4001                "idToken": "totp-signin-token",
4002                "refreshToken": "totp-signin-refresh"
4003            }));
4004        });
4005
4006        let assertion = TotpMultiFactorGenerator::assertion_for_sign_in("totp1", "123456");
4007        let result = resolver
4008            .resolve_sign_in(assertion)
4009            .await
4010            .expect("multi-factor sign-in should succeed");
4011
4012        finalize_mock.assert();
4013        assert_eq!(result.user.uid(), TEST_UID);
4014        assert_eq!(
4015            result.provider_id.as_deref(),
4016            Some(EmailAuthProvider::PROVIDER_ID)
4017        );
4018        assert_eq!(
4019            result.user.token_manager().access_token(),
4020            Some("totp-signin-token".to_string())
4021        );
4022    }
4023
4024    #[tokio::test(flavor = "current_thread")]
4025    async fn create_user_with_email_and_password_success() {
4026        let server = start_mock_server();
4027        let auth = build_auth(&server);
4028
4029        let mock = server.mock(|when, then| {
4030            when.method(POST)
4031                .path("/v1/accounts:signUp")
4032                .query_param("key", TEST_API_KEY)
4033                .json_body(json!({
4034                    "email": "user@example.com",
4035                    "password": "secret",
4036                    "returnSecureToken": true
4037                }));
4038            then.status(200).json_body(json!({
4039                "localId": "uid-456",
4040                "email": "user@example.com",
4041                "idToken": "new-id-token",
4042                "refreshToken": "new-refresh-token",
4043                "expiresIn": "7200"
4044            }));
4045        });
4046
4047        let credential = auth
4048            .create_user_with_email_and_password("user@example.com", "secret")
4049            .await
4050            .expect("sign-up should succeed");
4051
4052        mock.assert();
4053        assert_eq!(
4054            credential.provider_id.as_deref(),
4055            Some(EmailAuthProvider::PROVIDER_ID)
4056        );
4057        assert_eq!(credential.operation_type.as_deref(), Some("signUp"));
4058        assert_eq!(credential.user.uid(), "uid-456");
4059        assert_eq!(
4060            credential.user.token_manager().access_token(),
4061            Some("new-id-token".to_string())
4062        );
4063        assert_eq!(
4064            credential.user.refresh_token(),
4065            Some("new-refresh-token".to_string())
4066        );
4067    }
4068
4069    #[tokio::test(flavor = "current_thread")]
4070    async fn sign_in_with_invalid_expires_in_returns_error() {
4071        let server = start_mock_server();
4072        let auth = build_auth(&server);
4073
4074        let mock = server.mock(|when, then| {
4075            when.method(POST)
4076                .path("/v1/accounts:signInWithPassword")
4077                .query_param("key", TEST_API_KEY)
4078                .json_body(json!({
4079                    "email": "user@example.com",
4080                    "password": "secret",
4081                    "returnSecureToken": true
4082                }));
4083            then.status(200).json_body(json!({
4084                "localId": "uid-123",
4085                "email": "user@example.com",
4086                "idToken": "id-token",
4087                "refreshToken": "refresh-token",
4088                "expiresIn": "not-a-number"
4089            }));
4090        });
4091
4092        let result = auth
4093            .sign_in_with_email_and_password("user@example.com", "secret")
4094            .await;
4095
4096        mock.assert();
4097        assert!(matches!(
4098            result,
4099            Err(AuthError::InvalidCredential(message)) if message.contains("Invalid expiresIn value")
4100        ));
4101    }
4102
4103    #[tokio::test(flavor = "current_thread")]
4104    async fn sign_in_propagates_http_errors() {
4105        let server = start_mock_server();
4106        let auth = build_auth(&server);
4107
4108        let mock = server.mock(|when, then| {
4109            when.method(POST)
4110                .path("/v1/accounts:signInWithPassword")
4111                .query_param("key", TEST_API_KEY);
4112            then.status(400)
4113                .body("{\"error\":{\"message\":\"INVALID_PASSWORD\"}}");
4114        });
4115
4116        let result = auth
4117            .sign_in_with_email_and_password("user@example.com", "wrong-password")
4118            .await;
4119
4120        mock.assert();
4121        assert!(matches!(
4122            result,
4123            Err(AuthError::Network(message)) if message.contains("INVALID_PASSWORD")
4124        ));
4125    }
4126
4127    #[tokio::test(flavor = "current_thread")]
4128    async fn send_password_reset_email_sends_request_body() {
4129        let server = start_mock_server();
4130        let auth = build_auth(&server);
4131
4132        let mock = server.mock(|when, then| {
4133            when.method(POST)
4134                .path("/v1/accounts:sendOobCode")
4135                .query_param("key", TEST_API_KEY)
4136                .json_body(json!({
4137                    "requestType": "PASSWORD_RESET",
4138                    "email": TEST_EMAIL
4139                }));
4140            then.status(200);
4141        });
4142
4143        auth.send_password_reset_email(TEST_EMAIL)
4144            .await
4145            .expect("password reset should succeed");
4146
4147        mock.assert();
4148    }
4149
4150    #[tokio::test(flavor = "current_thread")]
4151    async fn send_sign_in_link_to_email_posts_expected_body() {
4152        let server = start_mock_server();
4153        let auth = build_auth(&server);
4154
4155        let settings = ActionCodeSettings {
4156            url: "https://example.com/finish".into(),
4157            handle_code_in_app: true,
4158            i_os: Some(IosSettings {
4159                bundle_id: "com.example.ios".into(),
4160            }),
4161            android: Some(AndroidSettings {
4162                package_name: "com.example.android".into(),
4163                install_app: Some(true),
4164                minimum_version: Some("12".into()),
4165            }),
4166            dynamic_link_domain: Some("example.page.link".into()),
4167            link_domain: Some("example.firebaseapp.com".into()),
4168        };
4169
4170        let mock = server.mock(|when, then| {
4171            when.method(POST)
4172                .path("/v1/accounts:sendOobCode")
4173                .query_param("key", TEST_API_KEY)
4174                .json_body(json!({
4175                    "requestType": "EMAIL_SIGNIN",
4176                    "email": TEST_EMAIL,
4177                    "continueUrl": "https://example.com/finish",
4178                    "dynamicLinkDomain": "example.page.link",
4179                    "linkDomain": "example.firebaseapp.com",
4180                    "canHandleCodeInApp": true,
4181                    "clientType": "CLIENT_TYPE_WEB",
4182                    "iOSBundleId": "com.example.ios",
4183                    "androidPackageName": "com.example.android",
4184                    "androidInstallApp": true,
4185                    "androidMinimumVersionCode": "12"
4186                }));
4187            then.status(200);
4188        });
4189
4190        auth.send_sign_in_link_to_email(TEST_EMAIL, &settings)
4191            .await
4192            .expect("sign-in link should be sent");
4193
4194        mock.assert();
4195    }
4196
4197    #[test]
4198    fn is_sign_in_with_email_link_checks_operation() {
4199        let server = start_mock_server();
4200        let auth = build_auth(&server);
4201
4202        let valid_link = format!(
4203            "https://example.com/action?apiKey={}&oobCode=oob-code&mode=signIn",
4204            TEST_API_KEY
4205        );
4206        assert!(auth.is_sign_in_with_email_link(&valid_link));
4207
4208        let invalid_link = format!(
4209            "https://example.com/action?apiKey={}&oobCode=oob-code&mode=verifyEmail",
4210            TEST_API_KEY
4211        );
4212        assert!(!auth.is_sign_in_with_email_link(&invalid_link));
4213    }
4214
4215    #[tokio::test(flavor = "current_thread")]
4216    async fn sign_in_with_email_link_success() {
4217        let server = start_mock_server();
4218        let auth = build_auth(&server);
4219
4220        let email_link = format!(
4221            "https://example.com/action?apiKey={}&oobCode=oob-code&mode=signIn",
4222            TEST_API_KEY
4223        );
4224
4225        let mock = server.mock(|when, then| {
4226            when.method(POST)
4227                .path("/v1/accounts:signInWithEmailLink")
4228                .query_param("key", TEST_API_KEY)
4229                .json_body(json!({
4230                    "email": TEST_EMAIL,
4231                    "oobCode": "oob-code",
4232                    "returnSecureToken": true
4233                }));
4234            then.status(200).json_body(json!({
4235                "localId": "email-link-uid",
4236                "email": TEST_EMAIL,
4237                "idToken": "email-link-id-token",
4238                "refreshToken": "email-link-refresh",
4239                "expiresIn": "3600"
4240            }));
4241        });
4242
4243        let credential = auth
4244            .sign_in_with_email_link(TEST_EMAIL, &email_link)
4245            .await
4246            .expect("email link sign-in should succeed");
4247
4248        mock.assert();
4249        assert_eq!(credential.user.uid(), "email-link-uid");
4250        assert_eq!(
4251            credential.provider_id.as_deref(),
4252            Some(EmailAuthProvider::PROVIDER_ID)
4253        );
4254    }
4255
4256    #[tokio::test(flavor = "current_thread")]
4257    async fn apply_action_code_posts_oob_code() {
4258        let server = start_mock_server();
4259        let auth = build_auth(&server);
4260
4261        let mock = server.mock(|when, then| {
4262            when.method(POST)
4263                .path("/v1/accounts:update")
4264                .query_param("key", TEST_API_KEY)
4265                .json_body(json!({
4266                    "oobCode": "action-code"
4267                }));
4268            then.status(200);
4269        });
4270
4271        auth.apply_action_code("action-code")
4272            .await
4273            .expect("apply action code should succeed");
4274
4275        mock.assert();
4276    }
4277
4278    #[tokio::test(flavor = "current_thread")]
4279    async fn check_action_code_returns_info() {
4280        let server = start_mock_server();
4281        let auth = build_auth(&server);
4282
4283        let mock = server.mock(|when, then| {
4284            when.method(POST)
4285                .path("/v1/accounts:resetPassword")
4286                .query_param("key", TEST_API_KEY)
4287                .json_body(json!({
4288                    "oobCode": "reset-code"
4289                }));
4290            then.status(200).json_body(json!({
4291                "email": TEST_EMAIL,
4292                "requestType": "PASSWORD_RESET"
4293            }));
4294        });
4295
4296        let info = auth
4297            .check_action_code("reset-code")
4298            .await
4299            .expect("check action code should succeed");
4300
4301        mock.assert();
4302        assert_eq!(info.operation, ActionCodeOperation::PasswordReset);
4303        assert_eq!(info.data.email.as_deref(), Some(TEST_EMAIL));
4304    }
4305
4306    #[tokio::test(flavor = "current_thread")]
4307    async fn verify_password_reset_code_returns_email() {
4308        let server = start_mock_server();
4309        let auth = build_auth(&server);
4310
4311        let mock = server.mock(|when, then| {
4312            when.method(POST)
4313                .path("/v1/accounts:resetPassword")
4314                .query_param("key", TEST_API_KEY)
4315                .json_body(json!({
4316                    "oobCode": "reset-code"
4317                }));
4318            then.status(200).json_body(json!({
4319                "email": TEST_EMAIL,
4320                "requestType": "PASSWORD_RESET"
4321            }));
4322        });
4323
4324        let email = auth
4325            .verify_password_reset_code("reset-code")
4326            .await
4327            .expect("verify password reset code should succeed");
4328
4329        mock.assert();
4330        assert_eq!(email, TEST_EMAIL);
4331    }
4332
4333    #[tokio::test(flavor = "current_thread")]
4334    async fn sign_in_with_phone_number_flow() {
4335        let server = start_mock_server();
4336        let auth = build_auth(&server);
4337
4338        let verifier: Arc<dyn ApplicationVerifier> = Arc::new(StaticVerifier {
4339            token: "recaptcha-token",
4340            kind: "recaptcha",
4341        });
4342
4343        let send_mock = server.mock(|when, then| {
4344            when.method(POST)
4345                .path("/v1/accounts:sendVerificationCode")
4346                .query_param("key", TEST_API_KEY)
4347                .json_body(json!({
4348                    "phoneNumber": "+15551234567",
4349                    "recaptchaToken": "recaptcha-token",
4350                    "clientType": CLIENT_TYPE_WEB
4351                }));
4352            then.status(200)
4353                .json_body(json!({ "sessionInfo": "session-info" }));
4354        });
4355
4356        let confirmation = auth
4357            .sign_in_with_phone_number("+15551234567", verifier.clone())
4358            .await
4359            .expect("send verification should succeed");
4360        send_mock.assert();
4361
4362        let finalize_mock = server.mock(|when, then| {
4363            when.method(POST)
4364                .path("/v1/accounts:signInWithPhoneNumber")
4365                .query_param("key", TEST_API_KEY)
4366                .json_body(json!({
4367                    "sessionInfo": "session-info",
4368                    "code": "123456"
4369                }));
4370            then.status(200).json_body(json!({
4371                "localId": "phone-uid",
4372                "idToken": "phone-id-token",
4373                "refreshToken": "phone-refresh-token",
4374                "expiresIn": "3600",
4375                "phoneNumber": "+15551234567",
4376                "isNewUser": false
4377            }));
4378        });
4379
4380        let lookup_mock = server.mock(|when, then| {
4381            when.method(POST)
4382                .path("/v1/accounts:lookup")
4383                .query_param("key", TEST_API_KEY)
4384                .json_body(json!({ "idToken": "phone-id-token" }));
4385            then.status(200).json_body(json!({
4386                "users": [{
4387                    "localId": "phone-uid",
4388                    "email": TEST_EMAIL,
4389                    "emailVerified": true,
4390                    "phoneNumber": "+15551234567",
4391                    "mfaInfo": []
4392                }]
4393            }));
4394        });
4395
4396        let credential = confirmation
4397            .confirm("123456")
4398            .await
4399            .expect("phone confirmation should succeed");
4400        finalize_mock.assert();
4401        lookup_mock.assert();
4402
4403        assert_eq!(credential.user.uid(), "phone-uid");
4404        assert_eq!(credential.provider_id.as_deref(), Some(PHONE_PROVIDER_ID));
4405        assert_eq!(credential.operation_type.as_deref(), Some("signIn"));
4406        assert_eq!(
4407            credential.user.info().phone_number.as_deref(),
4408            Some("+15551234567")
4409        );
4410    }
4411
4412    #[tokio::test(flavor = "current_thread")]
4413    async fn phone_auth_provider_sign_in_with_credential() {
4414        let server = start_mock_server();
4415        let auth = build_auth(&server);
4416
4417        let verifier: Arc<dyn ApplicationVerifier> = Arc::new(StaticVerifier {
4418            token: "recaptcha-token",
4419            kind: "recaptcha",
4420        });
4421
4422        let send_mock = server.mock(|when, then| {
4423            when.method(POST)
4424                .path("/v1/accounts:sendVerificationCode")
4425                .query_param("key", TEST_API_KEY)
4426                .json_body(json!({
4427                    "phoneNumber": "+15551234567",
4428                    "recaptchaToken": "recaptcha-token",
4429                    "clientType": CLIENT_TYPE_WEB
4430                }));
4431            then.status(200)
4432                .json_body(json!({ "sessionInfo": "provider-session" }));
4433        });
4434
4435        let provider = PhoneAuthProvider::new(auth.clone());
4436        let verification_id = provider
4437            .verify_phone_number("+15551234567", verifier.clone())
4438            .await
4439            .expect("verification should succeed");
4440        assert_eq!(verification_id, "provider-session");
4441        send_mock.assert();
4442
4443        let finalize_mock = server.mock(|when, then| {
4444            when.method(POST)
4445                .path("/v1/accounts:signInWithPhoneNumber")
4446                .query_param("key", TEST_API_KEY)
4447                .json_body(json!({
4448                    "sessionInfo": "provider-session",
4449                    "code": "123456"
4450                }));
4451            then.status(200).json_body(json!({
4452                "localId": "phone-uid",
4453                "idToken": "phone-id-token",
4454                "refreshToken": "phone-refresh-token",
4455                "expiresIn": "3600",
4456                "phoneNumber": "+15551234567",
4457                "isNewUser": false
4458            }));
4459        });
4460
4461        let lookup_mock = server.mock(|when, then| {
4462            when.method(POST)
4463                .path("/v1/accounts:lookup")
4464                .query_param("key", TEST_API_KEY)
4465                .json_body(json!({ "idToken": "phone-id-token" }));
4466            then.status(200).json_body(json!({
4467                "users": [{
4468                    "localId": "phone-uid",
4469                    "email": TEST_EMAIL,
4470                    "emailVerified": true,
4471                    "phoneNumber": "+15551234567",
4472                    "mfaInfo": []
4473                }]
4474            }));
4475        });
4476
4477        let credential = PhoneAuthProvider::credential(&verification_id, "123456");
4478        let result = provider
4479            .sign_in_with_credential(credential)
4480            .await
4481            .expect("sign-in should succeed");
4482
4483        finalize_mock.assert();
4484        lookup_mock.assert();
4485
4486        assert_eq!(result.user.uid(), "phone-uid");
4487        assert_eq!(result.provider_id.as_deref(), Some(PHONE_PROVIDER_ID));
4488        assert_eq!(result.operation_type.as_deref(), Some("signIn"));
4489    }
4490
4491    #[tokio::test(flavor = "current_thread")]
4492    async fn multi_factor_phone_enrollment_flow() {
4493        let server = start_mock_server();
4494        let auth = build_auth(&server);
4495        sign_in_user(&auth, &server).await;
4496
4497        let verifier: Arc<dyn ApplicationVerifier> = Arc::new(StaticVerifier {
4498            token: "recaptcha-token",
4499            kind: "recaptcha",
4500        });
4501
4502        let enroll_start = server.mock(|when, then| {
4503            when.method(POST)
4504                .path("/v1/accounts/mfaEnrollment:start")
4505                .query_param("key", TEST_API_KEY)
4506                .json_body(json!({
4507                    "idToken": TEST_ID_TOKEN,
4508                    "phoneEnrollmentInfo": {
4509                        "phoneNumber": "+15551234567",
4510                        "recaptchaToken": "recaptcha-token",
4511                        "clientType": CLIENT_TYPE_WEB
4512                    }
4513                }));
4514            then.status(200).json_body(json!({
4515                "phoneSessionInfo": {"sessionInfo": "mfa-session"}
4516            }));
4517        });
4518
4519        let enroll_finalize = server.mock(|when, then| {
4520            when.method(POST)
4521                .path("/v1/accounts/mfaEnrollment:finalize")
4522                .query_param("key", TEST_API_KEY)
4523                .json_body(json!({
4524                    "idToken": TEST_ID_TOKEN,
4525                    "phoneVerificationInfo": {
4526                        "sessionInfo": "mfa-session",
4527                        "code": "654321"
4528                    },
4529                    "displayName": "Personal phone"
4530                }));
4531            then.status(200).json_body(json!({
4532                "idToken": "mfa-id-token",
4533                "refreshToken": "mfa-refresh-token"
4534            }));
4535        });
4536
4537        let lookup = server.mock(|when, then| {
4538            when.method(POST)
4539                .path("/v1/accounts:lookup")
4540                .query_param("key", TEST_API_KEY)
4541                .json_body(json!({"idToken": "mfa-id-token"}));
4542            then.status(200).json_body(json!({
4543                "users": [{
4544                    "localId": TEST_UID,
4545                    "email": TEST_EMAIL,
4546                    "emailVerified": true,
4547                    "mfaInfo": [{
4548                        "mfaEnrollmentId": "enrollment-id",
4549                        "displayName": "Personal phone",
4550                        "phoneInfo": "+15551234567",
4551                        "enrolledAt": "2023-01-01T00:00:00Z"
4552                    }]
4553                }]
4554            }));
4555        });
4556
4557        let mfa_user = auth.multi_factor();
4558        let confirmation = mfa_user
4559            .enroll_phone_number("+15551234567", verifier, Some("Personal phone"))
4560            .await
4561            .expect("start MFA enrollment");
4562        enroll_start.assert();
4563
4564        let credential = confirmation
4565            .confirm("654321")
4566            .await
4567            .expect("finalize MFA enrollment");
4568
4569        enroll_finalize.assert();
4570        lookup.assert();
4571
4572        assert_eq!(credential.operation_type.as_deref(), Some("enroll"));
4573        assert_eq!(credential.provider_id.as_deref(), Some(PHONE_PROVIDER_ID));
4574
4575        let factors = mfa_user
4576            .enrolled_factors()
4577            .await
4578            .expect("load enrolled factors");
4579        assert_eq!(factors.len(), 1);
4580        assert_eq!(factors[0].uid, "enrollment-id");
4581        assert_eq!(factors[0].factor_id, "phone");
4582
4583        let session = mfa_user.get_session().await.expect("create session");
4584        assert_eq!(session.credential(), "mfa-id-token");
4585    }
4586
4587    #[tokio::test(flavor = "current_thread")]
4588    async fn send_email_verification_uses_current_user_token() {
4589        let server = start_mock_server();
4590        let auth = build_auth(&server);
4591        sign_in_user(&auth, &server).await;
4592
4593        let mock = server.mock(|when, then| {
4594            when.method(POST)
4595                .path("/v1/accounts:sendOobCode")
4596                .query_param("key", TEST_API_KEY)
4597                .json_body(json!({
4598                    "requestType": "VERIFY_EMAIL",
4599                    "idToken": TEST_ID_TOKEN
4600                }));
4601            then.status(200);
4602        });
4603
4604        auth.send_email_verification()
4605            .await
4606            .expect("email verification should succeed");
4607
4608        mock.assert();
4609    }
4610
4611    #[tokio::test(flavor = "current_thread")]
4612    async fn confirm_password_reset_posts_new_password() {
4613        let server = start_mock_server();
4614        let auth = build_auth(&server);
4615
4616        let mock = server.mock(|when, then| {
4617            when.method(POST)
4618                .path("/v1/accounts:resetPassword")
4619                .query_param("key", TEST_API_KEY)
4620                .json_body(json!({
4621                    "oobCode": "reset-code",
4622                    "newPassword": "new-secret"
4623                }));
4624            then.status(200);
4625        });
4626
4627        auth.confirm_password_reset("reset-code", "new-secret")
4628            .await
4629            .expect("confirm reset should succeed");
4630
4631        mock.assert();
4632    }
4633
4634    #[tokio::test(flavor = "current_thread")]
4635    async fn update_profile_sets_display_name() {
4636        let server = start_mock_server();
4637        let auth = build_auth(&server);
4638        sign_in_user(&auth, &server).await;
4639
4640        let mock = server.mock(|when, then| {
4641            when.method(POST)
4642                .path("/v1/accounts:update")
4643                .query_param("key", TEST_API_KEY)
4644                .json_body(json!({
4645                    "idToken": TEST_ID_TOKEN,
4646                    "displayName": "New Name",
4647                    "returnSecureToken": true
4648                }));
4649            then.status(200).json_body(json!({
4650                "idToken": TEST_ID_TOKEN,
4651                "refreshToken": TEST_REFRESH_TOKEN,
4652                "expiresIn": "3600",
4653                "localId": TEST_UID,
4654                "email": TEST_EMAIL,
4655                "displayName": "New Name"
4656            }));
4657        });
4658
4659        let user = auth
4660            .update_profile(Some("New Name"), None)
4661            .await
4662            .expect("update profile should succeed");
4663
4664        mock.assert();
4665        assert_eq!(user.info().display_name.as_deref(), Some("New Name"));
4666        assert_eq!(
4667            user.token_manager().access_token(),
4668            Some(TEST_ID_TOKEN.to_string())
4669        );
4670    }
4671
4672    #[tokio::test(flavor = "current_thread")]
4673    async fn update_profile_clears_display_name_when_empty_string() {
4674        let server = start_mock_server();
4675        let auth = build_auth(&server);
4676        sign_in_user(&auth, &server).await;
4677
4678        let set_mock = server.mock(|when, then| {
4679            when.method(POST)
4680                .path("/v1/accounts:update")
4681                .query_param("key", TEST_API_KEY)
4682                .json_body(json!({
4683                    "idToken": TEST_ID_TOKEN,
4684                    "displayName": "Existing",
4685                    "returnSecureToken": true
4686                }));
4687            then.status(200).json_body(json!({
4688                "idToken": TEST_ID_TOKEN,
4689                "refreshToken": TEST_REFRESH_TOKEN,
4690                "expiresIn": "3600",
4691                "localId": TEST_UID,
4692                "email": TEST_EMAIL,
4693                "displayName": "Existing"
4694            }));
4695        });
4696
4697        auth.update_profile(Some("Existing"), None)
4698            .await
4699            .expect("initial update should succeed");
4700        set_mock.assert();
4701
4702        let clear_mock = server.mock(|when, then| {
4703            when.method(POST)
4704                .path("/v1/accounts:update")
4705                .query_param("key", TEST_API_KEY)
4706                .json_body(json!({
4707                    "idToken": TEST_ID_TOKEN,
4708                    "deleteAttribute": ["DISPLAY_NAME"],
4709                    "returnSecureToken": true
4710                }));
4711            then.status(200).json_body(json!({
4712                "idToken": TEST_ID_TOKEN,
4713                "refreshToken": TEST_REFRESH_TOKEN,
4714                "expiresIn": "3600",
4715                "localId": TEST_UID,
4716                "email": TEST_EMAIL,
4717                "displayName": ""
4718            }));
4719        });
4720
4721        let user = auth
4722            .update_profile(Some(""), None)
4723            .await
4724            .expect("clear update should succeed");
4725
4726        clear_mock.assert();
4727        assert!(user.info().display_name.is_none());
4728    }
4729
4730    #[tokio::test(flavor = "current_thread")]
4731    async fn update_email_sets_new_email() {
4732        let server = start_mock_server();
4733        let auth = build_auth(&server);
4734        sign_in_user(&auth, &server).await;
4735
4736        let mock = server.mock(|when, then| {
4737            when.method(POST)
4738                .path("/v1/accounts:update")
4739                .query_param("key", TEST_API_KEY)
4740                .json_body(json!({
4741                    "idToken": TEST_ID_TOKEN,
4742                    "email": "new@example.com",
4743                    "returnSecureToken": true
4744                }));
4745            then.status(200).json_body(json!({
4746                "idToken": UPDATED_ID_TOKEN,
4747                "refreshToken": UPDATED_REFRESH_TOKEN,
4748                "expiresIn": "3600",
4749                "localId": TEST_UID,
4750                "email": "new@example.com"
4751            }));
4752        });
4753
4754        let user = auth
4755            .update_email("new@example.com")
4756            .await
4757            .expect("update email should succeed");
4758
4759        mock.assert();
4760        assert_eq!(user.info().email.as_deref(), Some("new@example.com"));
4761        assert_eq!(
4762            user.token_manager().access_token(),
4763            Some(UPDATED_ID_TOKEN.to_string())
4764        );
4765    }
4766
4767    #[tokio::test(flavor = "current_thread")]
4768    async fn update_password_refreshes_tokens() {
4769        let server = start_mock_server();
4770        let auth = build_auth(&server);
4771        sign_in_user(&auth, &server).await;
4772
4773        let mock = server.mock(|when, then| {
4774            when.method(POST)
4775                .path("/v1/accounts:update")
4776                .query_param("key", TEST_API_KEY)
4777                .json_body(json!({
4778                    "idToken": TEST_ID_TOKEN,
4779                    "password": "new-secret",
4780                    "returnSecureToken": true
4781                }));
4782            then.status(200).json_body(json!({
4783                "idToken": UPDATED_ID_TOKEN,
4784                "refreshToken": UPDATED_REFRESH_TOKEN,
4785                "expiresIn": "3600",
4786                "localId": TEST_UID,
4787                "email": TEST_EMAIL
4788            }));
4789        });
4790
4791        let user = auth
4792            .update_password("new-secret")
4793            .await
4794            .expect("update password should succeed");
4795
4796        mock.assert();
4797        assert_eq!(user.uid(), TEST_UID);
4798        assert_eq!(
4799            user.token_manager().refresh_token(),
4800            Some(UPDATED_REFRESH_TOKEN.to_string())
4801        );
4802    }
4803
4804    #[tokio::test(flavor = "current_thread")]
4805    async fn delete_user_clears_current_user_state() {
4806        let server = start_mock_server();
4807        let auth = build_auth(&server);
4808        sign_in_user(&auth, &server).await;
4809
4810        let mock = server.mock(|when, then| {
4811            when.method(POST)
4812                .path("/v1/accounts:delete")
4813                .query_param("key", TEST_API_KEY)
4814                .json_body(json!({
4815                    "idToken": TEST_ID_TOKEN
4816                }));
4817            then.status(200);
4818        });
4819
4820        auth.delete_user()
4821            .await
4822            .expect("delete user should succeed");
4823
4824        mock.assert();
4825        assert!(auth.current_user().is_none());
4826    }
4827
4828    #[tokio::test(flavor = "current_thread")]
4829    async fn reauthenticate_with_password_updates_current_user() {
4830        let server = start_mock_server();
4831        let auth = build_auth(&server);
4832        sign_in_user(&auth, &server).await;
4833
4834        let mock = server.mock(|when, then| {
4835            when.method(POST)
4836                .path("/v1/accounts:signInWithPassword")
4837                .query_param("key", TEST_API_KEY)
4838                .json_body(json!({
4839                    "email": REAUTH_EMAIL,
4840                    "password": TEST_PASSWORD,
4841                    "returnSecureToken": true
4842                }));
4843            then.status(200).json_body(json!({
4844                "localId": REAUTH_UID,
4845                "email": REAUTH_EMAIL,
4846                "idToken": REAUTH_ID_TOKEN,
4847                "refreshToken": REAUTH_REFRESH_TOKEN,
4848                "expiresIn": "3600"
4849            }));
4850        });
4851
4852        let user = auth
4853            .reauthenticate_with_password(REAUTH_EMAIL, TEST_PASSWORD)
4854            .await
4855            .expect("reauth should succeed");
4856
4857        mock.assert();
4858        assert_eq!(user.uid(), REAUTH_UID);
4859        assert_eq!(user.info().email.as_deref(), Some(REAUTH_EMAIL));
4860        assert_eq!(
4861            user.token_manager().access_token(),
4862            Some(REAUTH_ID_TOKEN.to_string())
4863        );
4864        assert_eq!(
4865            auth.current_user().expect("current user set").uid(),
4866            REAUTH_UID
4867        );
4868    }
4869
4870    #[tokio::test(flavor = "current_thread")]
4871    async fn unlink_providers_sends_delete_provider() {
4872        let server = start_mock_server();
4873        let auth = build_auth(&server);
4874        sign_in_user(&auth, &server).await;
4875
4876        let mock = server.mock(|when, then| {
4877            when.method(POST)
4878                .path("/v1/accounts:update")
4879                .query_param("key", TEST_API_KEY)
4880                .json_body(json!({
4881                    "idToken": TEST_ID_TOKEN,
4882                    "deleteProvider": [GOOGLE_PROVIDER_ID],
4883                    "returnSecureToken": true
4884                }));
4885            then.status(200).json_body(json!({
4886                "idToken": TEST_ID_TOKEN,
4887                "refreshToken": TEST_REFRESH_TOKEN,
4888                "expiresIn": "3600",
4889                "localId": TEST_UID,
4890                "email": TEST_EMAIL,
4891                "providerUserInfo": []
4892            }));
4893        });
4894
4895        let user = auth
4896            .unlink_providers(&[GOOGLE_PROVIDER_ID])
4897            .await
4898            .expect("unlink should succeed");
4899
4900        mock.assert();
4901        assert_eq!(user.uid(), TEST_UID);
4902        assert_eq!(user.info().provider_id, EmailAuthProvider::PROVIDER_ID);
4903    }
4904
4905    #[tokio::test(flavor = "current_thread")]
4906    async fn unlink_providers_propagates_errors() {
4907        let server = start_mock_server();
4908        let auth = build_auth(&server);
4909        sign_in_user(&auth, &server).await;
4910
4911        let mock = server.mock(|when, then| {
4912            when.method(POST)
4913                .path("/v1/accounts:update")
4914                .query_param("key", TEST_API_KEY);
4915            then.status(400)
4916                .body("{\"error\":{\"message\":\"INVALID_PROVIDER_ID\"}}");
4917        });
4918
4919        let result = auth.unlink_providers(&[GOOGLE_PROVIDER_ID]).await;
4920
4921        mock.assert();
4922        assert!(matches!(
4923            result,
4924            Err(AuthError::InvalidCredential(message)) if message == "INVALID_PROVIDER_ID"
4925        ));
4926    }
4927
4928    #[tokio::test(flavor = "current_thread")]
4929    async fn get_account_info_returns_users() {
4930        let server = start_mock_server();
4931        let auth = build_auth(&server);
4932        sign_in_user(&auth, &server).await;
4933
4934        let mock = server.mock(|when, then| {
4935            when.method(POST)
4936                .path("/v1/accounts:lookup")
4937                .query_param("key", TEST_API_KEY)
4938                .json_body(json!({
4939                    "idToken": TEST_ID_TOKEN
4940                }));
4941            then.status(200).json_body(json!({
4942                "users": [
4943                    {
4944                        "localId": TEST_UID,
4945                        "displayName": "my-name",
4946                        "email": TEST_EMAIL,
4947                        "providerUserInfo": [
4948                            {
4949                                "providerId": GOOGLE_PROVIDER_ID,
4950                                "email": TEST_EMAIL
4951                            }
4952                        ]
4953                    }
4954                ]
4955            }));
4956        });
4957
4958        let response = auth
4959            .get_account_info()
4960            .await
4961            .expect("get account info should succeed");
4962
4963        mock.assert();
4964        assert_eq!(response.users.len(), 1);
4965        assert_eq!(response.users[0].display_name.as_deref(), Some("my-name"));
4966        assert_eq!(response.users[0].email.as_deref(), Some(TEST_EMAIL));
4967        let providers = response.users[0]
4968            .provider_user_info
4969            .as_ref()
4970            .expect("providers present");
4971        assert_eq!(providers.len(), 1);
4972        assert_eq!(
4973            providers[0].provider_id.as_deref(),
4974            Some(GOOGLE_PROVIDER_ID)
4975        );
4976    }
4977
4978    #[tokio::test(flavor = "current_thread")]
4979    async fn get_account_info_propagates_errors() {
4980        let server = start_mock_server();
4981        let auth = build_auth(&server);
4982        sign_in_user(&auth, &server).await;
4983
4984        let mock = server.mock(|when, then| {
4985            when.method(POST)
4986                .path("/v1/accounts:lookup")
4987                .query_param("key", TEST_API_KEY);
4988            then.status(400)
4989                .body("{\"error\":{\"message\":\"INVALID_ID_TOKEN\"}}");
4990        });
4991
4992        let result = auth.get_account_info().await;
4993
4994        mock.assert();
4995        assert!(matches!(
4996            result,
4997            Err(AuthError::InvalidCredential(message)) if message == "INVALID_ID_TOKEN"
4998        ));
4999    }
5000}