firebase_rs_sdk/auth/
api.rs

1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::{Arc, Mutex, Weak};
3use std::thread;
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use reqwest::blocking::Client;
7use reqwest::Url;
8use serde::Serialize;
9use serde_json::Value;
10
11mod account;
12mod idp;
13pub mod token;
14
15use crate::app::AppError;
16use crate::app::FirebaseApp;
17use crate::auth::error::{AuthError, AuthResult};
18use crate::auth::model::{
19    AuthConfig, AuthCredential, AuthStateListeners, EmailAuthProvider, GetAccountInfoResponse,
20    SignInWithPasswordRequest, SignInWithPasswordResponse, SignUpRequest, SignUpResponse, User,
21    UserCredential, UserInfo,
22};
23use crate::auth::oauth::{
24    credential::OAuthCredential, InMemoryRedirectPersistence, OAuthPopupHandler,
25    OAuthRedirectHandler, PendingRedirectEvent, RedirectOperation, RedirectPersistence,
26};
27use crate::auth::persistence::{
28    AuthPersistence, InMemoryPersistence, PersistedAuthState, PersistenceListener,
29    PersistenceSubscription,
30};
31use crate::component::types::{
32    ComponentError, DynService, InstanceFactoryOptions, InstantiationMode,
33};
34use crate::component::{Component, ComponentContainer, ComponentType};
35use crate::firestore::remote::datastore::TokenProviderArc;
36use crate::util::{backoff, PartialObserver};
37use account::{
38    confirm_password_reset, delete_account, get_account_info, send_email_verification,
39    send_password_reset_email, update_account, verify_password, UpdateAccountRequest,
40    UpdateAccountResponse, UpdateString,
41};
42use idp::{sign_in_with_idp, SignInWithIdpRequest, SignInWithIdpResponse};
43
44const DEFAULT_OAUTH_REQUEST_URI: &str = "http://localhost";
45const DEFAULT_IDENTITY_TOOLKIT_ENDPOINT: &str = "https://identitytoolkit.googleapis.com/v1";
46
47pub struct Auth {
48    app: FirebaseApp,
49    config: Mutex<AuthConfig>,
50    current_user: Mutex<Option<Arc<User>>>,
51    listeners: AuthStateListeners,
52    rest_client: Client,
53    token_refresh_tolerance: Duration,
54    persistence: Arc<dyn AuthPersistence + Send + Sync>,
55    persisted_state_cache: Mutex<Option<PersistedAuthState>>,
56    persistence_subscription: Mutex<Option<PersistenceSubscription>>,
57    popup_handler: Mutex<Option<Arc<dyn OAuthPopupHandler>>>,
58    redirect_handler: Mutex<Option<Arc<dyn OAuthRedirectHandler>>>,
59    redirect_persistence: Mutex<Arc<dyn RedirectPersistence>>,
60    oauth_request_uri: Mutex<String>,
61    identity_toolkit_endpoint: Mutex<String>,
62    secure_token_endpoint: Mutex<String>,
63    refresh_cancel: Mutex<Option<Arc<AtomicBool>>>,
64    self_ref: Mutex<Weak<Auth>>,
65}
66
67impl Auth {
68    /// Creates a builder for configuring an `Auth` instance before construction.
69    pub fn builder(app: FirebaseApp) -> AuthBuilder {
70        AuthBuilder::new(app)
71    }
72
73    /// Constructs an `Auth` instance using in-memory persistence.
74    pub fn new(app: FirebaseApp) -> AuthResult<Self> {
75        Self::new_with_persistence(app, Arc::new(InMemoryPersistence::default()))
76    }
77
78    /// Constructs an `Auth` instance with a caller-provided persistence backend.
79    pub fn new_with_persistence(
80        app: FirebaseApp,
81        persistence: Arc<dyn AuthPersistence + Send + Sync>,
82    ) -> AuthResult<Self> {
83        let api_key = app
84            .options()
85            .api_key
86            .clone()
87            .ok_or_else(|| AuthError::InvalidCredential("Missing API key".into()))?;
88
89        let config = AuthConfig {
90            api_key: Some(api_key),
91            identity_toolkit_endpoint: Some(DEFAULT_IDENTITY_TOOLKIT_ENDPOINT.to_string()),
92            secure_token_endpoint: Some(token::DEFAULT_SECURE_TOKEN_ENDPOINT.to_string()),
93        };
94
95        Ok(Self {
96            app,
97            config: Mutex::new(config),
98            current_user: Mutex::new(None),
99            listeners: AuthStateListeners::default(),
100            rest_client: Client::new(),
101            token_refresh_tolerance: Duration::from_secs(5 * 60),
102            persistence,
103            persisted_state_cache: Mutex::new(None),
104            persistence_subscription: Mutex::new(None),
105            popup_handler: Mutex::new(None),
106            redirect_handler: Mutex::new(None),
107            redirect_persistence: Mutex::new(InMemoryRedirectPersistence::shared()),
108            oauth_request_uri: Mutex::new(DEFAULT_OAUTH_REQUEST_URI.to_string()),
109            identity_toolkit_endpoint: Mutex::new(DEFAULT_IDENTITY_TOOLKIT_ENDPOINT.to_string()),
110            secure_token_endpoint: Mutex::new(token::DEFAULT_SECURE_TOKEN_ENDPOINT.to_string()),
111            refresh_cancel: Mutex::new(None),
112            self_ref: Mutex::new(Weak::new()),
113        })
114    }
115
116    /// Finishes initialization by restoring persisted state and wiring listeners.
117    pub fn initialize(self: &Arc<Self>) -> AuthResult<()> {
118        *self.self_ref.lock().unwrap() = Arc::downgrade(self);
119        self.restore_from_persistence()?;
120        self.install_persistence_subscription()?;
121        Ok(())
122    }
123
124    /// Returns the `FirebaseApp` associated with this Auth instance.
125    pub fn app(&self) -> &FirebaseApp {
126        &self.app
127    }
128
129    /// Returns the currently signed-in user, if any.
130    pub fn current_user(&self) -> Option<Arc<User>> {
131        self.current_user.lock().unwrap().clone()
132    }
133
134    /// Signs out the current user and clears persisted credentials.
135    pub fn sign_out(&self) {
136        self.clear_local_user_state();
137        if let Err(err) = self.set_persisted_state(None) {
138            eprintln!("Failed to clear persisted auth state: {err}");
139        }
140    }
141
142    /// Returns the email/password auth provider helper.
143    pub fn email_auth_provider(&self) -> EmailAuthProvider {
144        EmailAuthProvider
145    }
146
147    /// Signs a user in using the email/password REST endpoint.
148    pub fn sign_in_with_email_and_password(
149        &self,
150        email: &str,
151        password: &str,
152    ) -> AuthResult<UserCredential> {
153        let api_key = self.api_key()?;
154
155        let request = SignInWithPasswordRequest {
156            email: email.to_owned(),
157            password: password.to_owned(),
158            return_secure_token: true,
159        };
160
161        let response: SignInWithPasswordResponse =
162            self.execute_request("accounts:signInWithPassword", &api_key, &request)?;
163
164        let expires_in = self.parse_expires_in(&response.expires_in)?;
165        let user = self.build_user_from_response(&response.local_id, &response.email);
166        user.update_tokens(
167            Some(response.id_token.clone()),
168            Some(response.refresh_token.clone()),
169            Some(expires_in),
170        );
171        let user_arc = Arc::new(user);
172        *self.current_user.lock().unwrap() = Some(user_arc.clone());
173        self.after_token_update(user_arc.clone())?;
174        self.listeners.notify(user_arc.clone());
175
176        Ok(UserCredential {
177            user: user_arc,
178            provider_id: Some(EmailAuthProvider::PROVIDER_ID.to_string()),
179            operation_type: Some("signIn".to_string()),
180        })
181    }
182
183    /// Creates a new user using email/password credentials.
184    pub fn create_user_with_email_and_password(
185        &self,
186        email: &str,
187        password: &str,
188    ) -> AuthResult<UserCredential> {
189        let api_key = self.api_key()?;
190
191        let request = SignUpRequest {
192            email: email.to_owned(),
193            password: password.to_owned(),
194            return_secure_token: true,
195        };
196
197        let response: SignUpResponse =
198            self.execute_request("accounts:signUp", &api_key, &request)?;
199
200        let user = self.build_user_from_response(&response.local_id, &response.email);
201        let expires_in = response
202            .expires_in
203            .as_ref()
204            .map(|expires| self.parse_expires_in(expires))
205            .transpose()?;
206        user.update_tokens(
207            Some(response.id_token.clone()),
208            Some(response.refresh_token.clone()),
209            expires_in,
210        );
211        let user_arc = Arc::new(user);
212        *self.current_user.lock().unwrap() = Some(user_arc.clone());
213        self.after_token_update(user_arc.clone())?;
214        self.listeners.notify(user_arc.clone());
215
216        Ok(UserCredential {
217            user: user_arc,
218            provider_id: Some(EmailAuthProvider::PROVIDER_ID.to_string()),
219            operation_type: Some("signUp".to_string()),
220        })
221    }
222
223    /// Registers an observer that is invoked whenever auth state changes.
224    pub fn on_auth_state_changed(
225        &self,
226        observer: PartialObserver<Arc<User>>,
227    ) -> impl FnOnce() + Send + 'static {
228        if let Some(user) = self.current_user() {
229            if let Some(next) = observer.next.clone() {
230                next(&user);
231            }
232        }
233
234        self.listeners.add_observer(observer);
235        || {}
236    }
237
238    fn execute_request<TRequest, TResponse>(
239        &self,
240        path: &str,
241        api_key: &str,
242        request: &TRequest,
243    ) -> AuthResult<TResponse>
244    where
245        TRequest: Serialize,
246        TResponse: serde::de::DeserializeOwned,
247    {
248        let url = self.endpoint_url(path, api_key)?;
249        let response = self
250            .rest_client
251            .post(url)
252            .json(request)
253            .send()
254            .map_err(|err| AuthError::Network(err.to_string()))?;
255
256        if !response.status().is_success() {
257            let message = response
258                .text()
259                .unwrap_or_else(|_| "Unknown error".to_string());
260            return Err(AuthError::Network(message));
261        }
262
263        response
264            .json()
265            .map_err(|err| AuthError::Network(err.to_string()))
266    }
267
268    fn endpoint_url(&self, path: &str, api_key: &str) -> AuthResult<Url> {
269        let base = self.identity_toolkit_endpoint();
270        let endpoint = format!("{}/{}?key={}", base.trim_end_matches('/'), path, api_key);
271        Url::parse(&endpoint).map_err(|err| AuthError::Network(err.to_string()))
272    }
273
274    fn build_user_from_response(&self, local_id: &str, email: &str) -> User {
275        let info = UserInfo {
276            uid: local_id.to_string(),
277            display_name: None,
278            email: Some(email.to_string()),
279            phone_number: None,
280            photo_url: None,
281            provider_id: EmailAuthProvider::PROVIDER_ID.to_string(),
282        };
283        User::new(self.app.clone(), info)
284    }
285
286    fn api_key(&self) -> AuthResult<String> {
287        self.config
288            .lock()
289            .unwrap()
290            .api_key
291            .clone()
292            .ok_or_else(|| AuthError::InvalidCredential("Missing API key".into()))
293    }
294
295    fn parse_expires_in(&self, value: &str) -> AuthResult<Duration> {
296        let seconds = value.parse::<u64>().map_err(|err| {
297            AuthError::InvalidCredential(format!("Invalid expiresIn value: {err}"))
298        })?;
299        Ok(Duration::from_secs(seconds))
300    }
301
302    fn refresh_user_token(&self, user: &Arc<User>) -> AuthResult<String> {
303        let refresh_token = user
304            .refresh_token()
305            .ok_or_else(|| AuthError::InvalidCredential("Missing refresh token".into()))?;
306        let api_key = self.api_key()?;
307        let secure_endpoint = self.secure_token_endpoint();
308        let response = token::refresh_id_token_with_endpoint(
309            &self.rest_client,
310            &secure_endpoint,
311            &api_key,
312            &refresh_token,
313        )?;
314        let expires_in = self.parse_expires_in(&response.expires_in)?;
315        user.update_tokens(
316            Some(response.id_token.clone()),
317            Some(response.refresh_token.clone()),
318            Some(expires_in),
319        );
320        self.after_token_update(user.clone())?;
321        self.listeners.notify(user.clone());
322        Ok(response.id_token)
323    }
324
325    /// Returns the current user's ID token, refreshing when requested.
326    pub fn get_token(&self, force_refresh: bool) -> AuthResult<Option<String>> {
327        let user = match self.current_user() {
328            Some(user) => user,
329            None => return Ok(None),
330        };
331
332        let needs_refresh = force_refresh
333            || user
334                .token_manager()
335                .should_refresh(self.token_refresh_tolerance);
336
337        if needs_refresh {
338            self.refresh_user_token(&user).map(Some)
339        } else {
340            Ok(user.token_manager().access_token())
341        }
342    }
343
344    /// Exposes this auth instance as a Firestore token provider.
345    pub fn token_provider(self: &Arc<Self>) -> TokenProviderArc {
346        crate::auth::token_provider::auth_token_provider_arc(self.clone())
347    }
348
349    /// Overrides the default OAuth request URI used during flows.
350    pub fn set_oauth_request_uri(&self, value: impl Into<String>) {
351        *self.oauth_request_uri.lock().unwrap() = value.into();
352    }
353
354    /// Returns the OAuth request URI for popup/redirect flows.
355    pub fn oauth_request_uri(&self) -> String {
356        self.oauth_request_uri.lock().unwrap().clone()
357    }
358
359    /// Updates the Identity Toolkit REST endpoint.
360    pub fn set_identity_toolkit_endpoint(&self, endpoint: impl Into<String>) {
361        let value = endpoint.into();
362        *self.identity_toolkit_endpoint.lock().unwrap() = value.clone();
363        self.config.lock().unwrap().identity_toolkit_endpoint = Some(value);
364    }
365
366    /// Returns the Identity Toolkit REST endpoint in use.
367    pub fn identity_toolkit_endpoint(&self) -> String {
368        self.identity_toolkit_endpoint.lock().unwrap().clone()
369    }
370
371    /// Sets the Secure Token endpoint used for refresh operations.
372    pub fn set_secure_token_endpoint(&self, endpoint: impl Into<String>) {
373        let value = endpoint.into();
374        *self.secure_token_endpoint.lock().unwrap() = value.clone();
375        self.config.lock().unwrap().secure_token_endpoint = Some(value);
376    }
377
378    fn secure_token_endpoint(&self) -> String {
379        self.secure_token_endpoint.lock().unwrap().clone()
380    }
381
382    /// Installs an OAuth popup handler implementation.
383    pub fn set_popup_handler(&self, handler: Arc<dyn OAuthPopupHandler>) {
384        *self.popup_handler.lock().unwrap() = Some(handler);
385    }
386
387    /// Clears any installed popup handler.
388    pub fn clear_popup_handler(&self) {
389        *self.popup_handler.lock().unwrap() = None;
390    }
391
392    /// Retrieves the currently configured popup handler.
393    pub fn popup_handler(&self) -> Option<Arc<dyn OAuthPopupHandler>> {
394        self.popup_handler.lock().unwrap().clone()
395    }
396
397    /// Installs an OAuth redirect handler implementation.
398    pub fn set_redirect_handler(&self, handler: Arc<dyn OAuthRedirectHandler>) {
399        *self.redirect_handler.lock().unwrap() = Some(handler);
400    }
401
402    /// Clears any installed redirect handler.
403    pub fn clear_redirect_handler(&self) {
404        *self.redirect_handler.lock().unwrap() = None;
405    }
406
407    /// Retrieves the currently configured redirect handler.
408    pub fn redirect_handler(&self) -> Option<Arc<dyn OAuthRedirectHandler>> {
409        self.redirect_handler.lock().unwrap().clone()
410    }
411
412    /// Replaces the persistence mechanism used for redirect state.
413    pub fn set_redirect_persistence(&self, persistence: Arc<dyn RedirectPersistence>) {
414        *self.redirect_persistence.lock().unwrap() = persistence;
415    }
416
417    fn redirect_persistence(&self) -> Arc<dyn RedirectPersistence> {
418        self.redirect_persistence.lock().unwrap().clone()
419    }
420
421    /// Signs in using an OAuth credential produced by popup/redirect flows.
422    pub fn sign_in_with_oauth_credential(
423        &self,
424        credential: AuthCredential,
425    ) -> AuthResult<UserCredential> {
426        self.exchange_oauth_credential(credential, None)
427    }
428
429    /// Sends a password reset email to the specified address.
430    pub fn send_password_reset_email(&self, email: &str) -> AuthResult<()> {
431        let api_key = self.api_key()?;
432        let endpoint = self.identity_toolkit_endpoint();
433        send_password_reset_email(&self.rest_client, &endpoint, &api_key, email)
434    }
435
436    /// Confirms a password reset OOB code and applies the new password.
437    pub fn confirm_password_reset(&self, oob_code: &str, new_password: &str) -> AuthResult<()> {
438        let api_key = self.api_key()?;
439        let endpoint = self.identity_toolkit_endpoint();
440        confirm_password_reset(
441            &self.rest_client,
442            &endpoint,
443            &api_key,
444            oob_code,
445            new_password,
446        )
447    }
448
449    /// Sends an email verification message to the currently signed-in user.
450    pub fn send_email_verification(&self) -> AuthResult<()> {
451        let user = self.require_current_user()?;
452        let id_token = user.get_id_token(false)?;
453        let api_key = self.api_key()?;
454        let endpoint = self.identity_toolkit_endpoint();
455        send_email_verification(&self.rest_client, &endpoint, &api_key, &id_token)
456    }
457
458    /// Updates the current user's display name and photo URL.
459    pub fn update_profile(
460        &self,
461        display_name: Option<&str>,
462        photo_url: Option<&str>,
463    ) -> AuthResult<Arc<User>> {
464        let user = self.require_current_user()?;
465        let id_token = user.get_id_token(false)?;
466        let mut request = UpdateAccountRequest::new(id_token);
467        if let Some(value) = display_name {
468            if value.is_empty() {
469                request.display_name = Some(UpdateString::Clear);
470            } else {
471                request.display_name = Some(UpdateString::Set(value.to_string()));
472            }
473        }
474        if let Some(value) = photo_url {
475            if value.is_empty() {
476                request.photo_url = Some(UpdateString::Clear);
477            } else {
478                request.photo_url = Some(UpdateString::Set(value.to_string()));
479            }
480        }
481
482        self.perform_account_update(user, request)
483    }
484
485    /// Updates the current user's email address.
486    pub fn update_email(&self, email: &str) -> AuthResult<Arc<User>> {
487        let user = self.require_current_user()?;
488        let id_token = user.get_id_token(false)?;
489        let mut request = UpdateAccountRequest::new(id_token);
490        request.email = Some(email.to_string());
491        self.perform_account_update(user, request)
492    }
493
494    /// Updates the current user's password.
495    pub fn update_password(&self, password: &str) -> AuthResult<Arc<User>> {
496        let user = self.require_current_user()?;
497        let id_token = user.get_id_token(false)?;
498        let mut request = UpdateAccountRequest::new(id_token);
499        request.password = Some(password.to_string());
500        self.perform_account_update(user, request)
501    }
502
503    /// Deletes the current user from Firebase Auth.
504    pub fn delete_user(&self) -> AuthResult<()> {
505        let user = self.require_current_user()?;
506        let id_token = user.get_id_token(false)?;
507        let api_key = self.api_key()?;
508        let endpoint = self.identity_toolkit_endpoint();
509        delete_account(&self.rest_client, &endpoint, &api_key, &id_token)?;
510        self.sign_out();
511        Ok(())
512    }
513
514    /// Unlinks the specified providers from the current user.
515    pub fn unlink_providers(&self, provider_ids: &[&str]) -> AuthResult<Arc<User>> {
516        let user = self.require_current_user()?;
517        let id_token = user.get_id_token(false)?;
518        let mut request = UpdateAccountRequest::new(id_token);
519        request.delete_providers = provider_ids.iter().map(|id| id.to_string()).collect();
520        self.perform_account_update(user, request)
521    }
522
523    /// Fetches the latest account info for the current user.
524    pub fn get_account_info(&self) -> AuthResult<GetAccountInfoResponse> {
525        let user = self.require_current_user()?;
526        let id_token = user.get_id_token(false)?;
527        let api_key = self.api_key()?;
528        let endpoint = self.identity_toolkit_endpoint();
529        get_account_info(&self.rest_client, &endpoint, &api_key, &id_token)
530    }
531
532    /// Links an OAuth credential with the currently signed-in user.
533    pub fn link_with_oauth_credential(
534        &self,
535        credential: AuthCredential,
536    ) -> AuthResult<UserCredential> {
537        let user = self.require_current_user()?;
538        let id_token = user.get_id_token(false)?;
539        self.exchange_oauth_credential(credential, Some(id_token))
540    }
541
542    /// Reauthenticates the current user with email and password.
543    pub fn reauthenticate_with_password(
544        &self,
545        email: &str,
546        password: &str,
547    ) -> AuthResult<Arc<User>> {
548        let request = SignInWithPasswordRequest {
549            email: email.to_string(),
550            password: password.to_string(),
551            return_secure_token: true,
552        };
553
554        let api_key = self.api_key()?;
555        let endpoint = self.identity_toolkit_endpoint();
556        let response = verify_password(&self.rest_client, &endpoint, &api_key, &request)?;
557        self.apply_password_reauth(response)
558    }
559
560    /// Reauthenticates the current user with an OAuth credential.
561    pub fn reauthenticate_with_oauth_credential(
562        &self,
563        credential: AuthCredential,
564    ) -> AuthResult<Arc<User>> {
565        let user = self.require_current_user()?;
566        let result = self.exchange_oauth_credential(credential, Some(user.get_id_token(false)?))?;
567        Ok(result.user)
568    }
569
570    fn exchange_oauth_credential(
571        &self,
572        credential: AuthCredential,
573        id_token: Option<String>,
574    ) -> AuthResult<UserCredential> {
575        let oauth_credential = OAuthCredential::try_from(credential)?;
576        let post_body = oauth_credential.build_post_body()?;
577        let request = SignInWithIdpRequest {
578            post_body,
579            request_uri: self.oauth_request_uri(),
580            return_idp_credential: true,
581            return_secure_token: true,
582            id_token,
583        };
584
585        let api_key = self.api_key()?;
586        let response = sign_in_with_idp(&self.rest_client, &api_key, &request)?;
587        let user_arc = self.upsert_user_from_idp_response(&response, &oauth_credential)?;
588        let provider_id = response
589            .provider_id
590            .clone()
591            .or_else(|| Some(oauth_credential.provider_id().to_string()))
592            .unwrap_or_else(|| EmailAuthProvider::PROVIDER_ID.to_string());
593
594        self.listeners.notify(user_arc.clone());
595
596        Ok(UserCredential {
597            user: user_arc,
598            provider_id: Some(provider_id),
599            operation_type: Some(if response.is_new_user.unwrap_or(false) {
600                "signUp".to_string()
601            } else {
602                "signIn".to_string()
603            }),
604        })
605    }
606
607    fn perform_account_update(
608        &self,
609        current_user: Arc<User>,
610        request: UpdateAccountRequest,
611    ) -> AuthResult<Arc<User>> {
612        let api_key = self.api_key()?;
613        let endpoint = self.identity_toolkit_endpoint();
614        let response = update_account(&self.rest_client, &endpoint, &api_key, &request)?;
615        let updated_user = self.apply_account_update(&current_user, &response)?;
616        self.listeners.notify(updated_user.clone());
617        Ok(updated_user)
618    }
619
620    fn require_current_user(&self) -> AuthResult<Arc<User>> {
621        self.current_user()
622            .ok_or_else(|| AuthError::InvalidCredential("No user signed in".into()))
623    }
624
625    pub(crate) fn set_pending_redirect_event(
626        &self,
627        provider_id: &str,
628        operation: RedirectOperation,
629    ) -> AuthResult<()> {
630        let event = PendingRedirectEvent {
631            provider_id: provider_id.to_string(),
632            operation,
633        };
634        self.redirect_persistence().set(Some(event))
635    }
636
637    pub(crate) fn clear_pending_redirect_event(&self) -> AuthResult<()> {
638        self.redirect_persistence().set(None)
639    }
640
641    pub(crate) fn take_pending_redirect_event(&self) -> AuthResult<Option<PendingRedirectEvent>> {
642        let event = self.redirect_persistence().get()?;
643        if event.is_some() {
644            self.redirect_persistence().set(None)?;
645        }
646        Ok(event)
647    }
648
649    fn apply_password_reauth(&self, response: SignInWithPasswordResponse) -> AuthResult<Arc<User>> {
650        let user = self.build_user_from_response(&response.local_id, &response.email);
651        let expires_in = self.parse_expires_in(&response.expires_in)?;
652        user.update_tokens(
653            Some(response.id_token.clone()),
654            Some(response.refresh_token.clone()),
655            Some(expires_in),
656        );
657
658        let user_arc = Arc::new(user);
659        *self.current_user.lock().unwrap() = Some(user_arc.clone());
660        self.after_token_update(user_arc.clone())?;
661        Ok(user_arc)
662    }
663
664    fn restore_from_persistence(&self) -> AuthResult<()> {
665        let state = self.persistence.get()?;
666        let notify = state.is_some();
667        self.sync_from_persistence(state, notify)
668    }
669
670    fn after_token_update(&self, user: Arc<User>) -> AuthResult<()> {
671        self.save_persisted_state(&user)?;
672        self.schedule_refresh_for_user(user);
673        Ok(())
674    }
675
676    fn save_persisted_state(&self, user: &Arc<User>) -> AuthResult<()> {
677        let refresh_token = match user.refresh_token() {
678            Some(token) if !token.is_empty() => Some(token),
679            _ => {
680                self.set_persisted_state(None)?;
681                return Ok(());
682            }
683        };
684
685        let expires_at = user
686            .token_manager()
687            .expiration_time()
688            .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
689            .map(|duration| duration.as_secs() as i64);
690
691        let state = PersistedAuthState {
692            user_id: user.uid().to_string(),
693            email: user.info().email.clone(),
694            refresh_token,
695            access_token: user.token_manager().access_token(),
696            expires_at,
697        };
698        self.set_persisted_state(Some(state))
699    }
700
701    fn set_persisted_state(&self, state: Option<PersistedAuthState>) -> AuthResult<()> {
702        {
703            let cache = self.persisted_state_cache.lock().unwrap();
704            if *cache == state {
705                return Ok(());
706            }
707        }
708
709        let previous = self.update_cached_state(state.clone());
710        if let Err(err) = self.persistence.set(state) {
711            self.update_cached_state(previous);
712            return Err(err);
713        }
714        Ok(())
715    }
716
717    fn update_cached_state(&self, state: Option<PersistedAuthState>) -> Option<PersistedAuthState> {
718        let mut guard = self.persisted_state_cache.lock().unwrap();
719        std::mem::replace(&mut *guard, state)
720    }
721
722    fn install_persistence_subscription(self: &Arc<Self>) -> AuthResult<()> {
723        let weak = Arc::downgrade(self);
724        let listener: PersistenceListener = Arc::new(move |state: Option<PersistedAuthState>| {
725            if let Some(auth) = weak.upgrade() {
726                if let Err(err) = auth.sync_from_persistence(state, true) {
727                    eprintln!("Failed to sync persisted auth state: {err}");
728                }
729            }
730        });
731
732        let subscription = self.persistence.subscribe(listener)?;
733        *self.persistence_subscription.lock().unwrap() = Some(subscription);
734        Ok(())
735    }
736
737    fn sync_from_persistence(
738        &self,
739        state: Option<PersistedAuthState>,
740        notify_listeners: bool,
741    ) -> AuthResult<()> {
742        {
743            let cache = self.persisted_state_cache.lock().unwrap();
744            if *cache == state {
745                return Ok(());
746            }
747        }
748
749        match state.clone() {
750            Some(ref persisted) if Self::has_refresh_token(persisted) => {
751                let user_arc = self.build_user_from_persisted_state(persisted);
752                *self.current_user.lock().unwrap() = Some(user_arc.clone());
753                self.schedule_refresh_for_user(user_arc.clone());
754                if notify_listeners {
755                    self.listeners.notify(user_arc);
756                }
757            }
758            _ => {
759                self.clear_local_user_state();
760            }
761        }
762
763        self.update_cached_state(state);
764        Ok(())
765    }
766
767    fn build_user_from_persisted_state(&self, state: &PersistedAuthState) -> Arc<User> {
768        let info = UserInfo {
769            uid: state.user_id.clone(),
770            display_name: None,
771            email: state.email.clone(),
772            phone_number: None,
773            photo_url: None,
774            provider_id: EmailAuthProvider::PROVIDER_ID.to_string(),
775        };
776
777        let user = User::new(self.app.clone(), info);
778        let expiration_time = state.expires_at.and_then(|seconds| {
779            if seconds <= 0 {
780                None
781            } else {
782                UNIX_EPOCH.checked_add(Duration::from_secs(seconds as u64))
783            }
784        });
785
786        user.token_manager().initialize(
787            state.access_token.clone(),
788            state.refresh_token.clone(),
789            expiration_time,
790        );
791
792        Arc::new(user)
793    }
794
795    fn clear_local_user_state(&self) {
796        self.cancel_scheduled_refresh();
797        let mut guard = self.current_user.lock().unwrap();
798        if let Some(user) = guard.as_ref() {
799            user.token_manager().clear();
800        }
801        *guard = None;
802    }
803
804    fn has_refresh_token(state: &PersistedAuthState) -> bool {
805        state
806            .refresh_token
807            .as_ref()
808            .map(|token| !token.is_empty())
809            .unwrap_or(false)
810    }
811
812    fn upsert_user_from_idp_response(
813        &self,
814        response: &SignInWithIdpResponse,
815        oauth_credential: &OAuthCredential,
816    ) -> AuthResult<Arc<User>> {
817        let id_token = response.id_token.clone().ok_or_else(|| {
818            AuthError::InvalidCredential("signInWithIdp response missing idToken".into())
819        })?;
820        let refresh_token = response.refresh_token.clone().ok_or_else(|| {
821            AuthError::InvalidCredential("signInWithIdp response missing refreshToken".into())
822        })?;
823        let local_id = response.local_id.clone().ok_or_else(|| {
824            AuthError::InvalidCredential("signInWithIdp response missing localId".into())
825        })?;
826
827        let provider_id = response
828            .provider_id
829            .clone()
830            .unwrap_or_else(|| oauth_credential.provider_id().to_string());
831
832        let display_name = oauth_credential
833            .token_response()
834            .get("displayName")
835            .and_then(Value::as_str)
836            .map(|value| value.to_string());
837        let photo_url = oauth_credential
838            .token_response()
839            .get("photoUrl")
840            .and_then(Value::as_str)
841            .map(|value| value.to_string());
842
843        let info = UserInfo {
844            uid: local_id,
845            display_name,
846            email: response.email.clone(),
847            phone_number: None,
848            photo_url,
849            provider_id,
850        };
851
852        let user = User::new(self.app.clone(), info);
853        let expires_in = response
854            .expires_in
855            .as_deref()
856            .map(|value| self.parse_expires_in(value))
857            .transpose()?;
858        user.update_tokens(Some(id_token), Some(refresh_token), expires_in);
859
860        let user_arc = Arc::new(user);
861        *self.current_user.lock().unwrap() = Some(user_arc.clone());
862        self.after_token_update(user_arc.clone())?;
863        Ok(user_arc)
864    }
865
866    fn apply_account_update(
867        &self,
868        current_user: &Arc<User>,
869        response: &UpdateAccountResponse,
870    ) -> AuthResult<Arc<User>> {
871        let id_token = response.id_token.clone().ok_or_else(|| {
872            AuthError::InvalidCredential("accounts:update response missing idToken".into())
873        })?;
874        let refresh_token = response.refresh_token.clone().ok_or_else(|| {
875            AuthError::InvalidCredential("accounts:update response missing refreshToken".into())
876        })?;
877
878        let expires_in = response
879            .expires_in
880            .as_deref()
881            .map(|value| self.parse_expires_in(value))
882            .transpose()?;
883
884        let uid = response
885            .local_id
886            .clone()
887            .unwrap_or_else(|| current_user.uid().to_string());
888
889        let email = response
890            .email
891            .clone()
892            .or_else(|| current_user.info().email.clone());
893
894        let display_name = match response.display_name.as_deref() {
895            Some(value) if value.is_empty() => None,
896            Some(value) => Some(value.to_string()),
897            None => current_user.info().display_name.clone(),
898        };
899
900        let photo_url = match response.photo_url.as_deref() {
901            Some(value) if value.is_empty() => None,
902            Some(value) => Some(value.to_string()),
903            None => current_user.info().photo_url.clone(),
904        };
905
906        let provider_id = response
907            .provider_user_info
908            .as_ref()
909            .and_then(|infos| infos.first())
910            .and_then(|info| info.provider_id.clone())
911            .unwrap_or_else(|| current_user.info().provider_id.clone());
912
913        let info = UserInfo {
914            uid,
915            display_name,
916            email,
917            phone_number: current_user.info().phone_number.clone(),
918            photo_url,
919            provider_id,
920        };
921
922        let user = User::new(self.app.clone(), info);
923        user.update_tokens(Some(id_token), Some(refresh_token), expires_in);
924
925        let user_arc = Arc::new(user);
926        *self.current_user.lock().unwrap() = Some(user_arc.clone());
927        self.after_token_update(user_arc.clone())?;
928        Ok(user_arc)
929    }
930
931    fn schedule_refresh_for_user(&self, user: Arc<User>) {
932        if user.refresh_token().is_none() {
933            self.cancel_scheduled_refresh();
934            return;
935        }
936
937        let Some(expiration_time) = user.token_manager().expiration_time() else {
938            self.cancel_scheduled_refresh();
939            return;
940        };
941
942        let now = SystemTime::now();
943        let expires_in = match expiration_time.duration_since(now) {
944            Ok(duration) => duration,
945            Err(_) => Duration::from_secs(0),
946        };
947
948        let delay = if expires_in > self.token_refresh_tolerance {
949            expires_in - self.token_refresh_tolerance
950        } else {
951            Duration::from_secs(0)
952        };
953
954        let Some(self_arc) = self.self_arc() else {
955            return;
956        };
957
958        let cancel_flag = Arc::new(AtomicBool::new(false));
959        {
960            let mut guard = self.refresh_cancel.lock().unwrap();
961            if let Some(flag) = guard.take() {
962                flag.store(true, Ordering::SeqCst);
963            }
964            *guard = Some(cancel_flag.clone());
965        }
966
967        let user_arc = user.clone();
968        thread::spawn(move || {
969            if !sleep_with_cancel(delay, &cancel_flag) {
970                return;
971            }
972
973            let mut attempts = 0u32;
974            loop {
975                if cancel_flag.load(Ordering::SeqCst) {
976                    return;
977                }
978
979                match self_arc.refresh_user_token(&user_arc) {
980                    Ok(_) => return,
981                    Err(err) => {
982                        attempts = attempts.saturating_add(1);
983                        let wait = backoff::calculate_backoff_millis(attempts);
984                        eprintln!("Auth token refresh failed (attempt {attempts}): {err}");
985                        if !sleep_with_cancel(Duration::from_millis(wait), &cancel_flag) {
986                            return;
987                        }
988                    }
989                }
990            }
991        });
992    }
993
994    fn cancel_scheduled_refresh(&self) {
995        if let Some(flag) = self.refresh_cancel.lock().unwrap().take() {
996            flag.store(true, Ordering::SeqCst);
997        }
998    }
999
1000    fn self_arc(&self) -> Option<Arc<Auth>> {
1001        self.self_ref.lock().unwrap().upgrade()
1002    }
1003}
1004
1005pub struct AuthBuilder {
1006    app: FirebaseApp,
1007    persistence: Option<Arc<dyn AuthPersistence + Send + Sync>>,
1008    auto_initialize: bool,
1009    popup_handler: Option<Arc<dyn OAuthPopupHandler>>,
1010    redirect_handler: Option<Arc<dyn OAuthRedirectHandler>>,
1011    oauth_request_uri: Option<String>,
1012    redirect_persistence: Option<Arc<dyn RedirectPersistence>>,
1013    identity_toolkit_endpoint: Option<String>,
1014    secure_token_endpoint: Option<String>,
1015}
1016
1017impl AuthBuilder {
1018    fn new(app: FirebaseApp) -> Self {
1019        Self {
1020            app,
1021            persistence: None,
1022            auto_initialize: true,
1023            popup_handler: None,
1024            redirect_handler: None,
1025            oauth_request_uri: None,
1026            redirect_persistence: None,
1027            identity_toolkit_endpoint: None,
1028            secure_token_endpoint: None,
1029        }
1030    }
1031
1032    /// Overrides the persistence backend used by the Auth instance.
1033    pub fn with_persistence(mut self, persistence: Arc<dyn AuthPersistence + Send + Sync>) -> Self {
1034        self.persistence = Some(persistence);
1035        self
1036    }
1037
1038    /// Installs a popup handler prior to building the Auth instance.
1039    pub fn with_popup_handler(mut self, handler: Arc<dyn OAuthPopupHandler>) -> Self {
1040        self.popup_handler = Some(handler);
1041        self
1042    }
1043
1044    /// Installs a redirect handler prior to building the Auth instance.
1045    pub fn with_redirect_handler(mut self, handler: Arc<dyn OAuthRedirectHandler>) -> Self {
1046        self.redirect_handler = Some(handler);
1047        self
1048    }
1049
1050    /// Overrides the default OAuth request URI before building.
1051    pub fn with_oauth_request_uri(mut self, request_uri: impl Into<String>) -> Self {
1052        self.oauth_request_uri = Some(request_uri.into());
1053        self
1054    }
1055
1056    /// Configures the redirect persistence implementation used post-build.
1057    pub fn with_redirect_persistence(mut self, persistence: Arc<dyn RedirectPersistence>) -> Self {
1058        self.redirect_persistence = Some(persistence);
1059        self
1060    }
1061
1062    /// Overrides the Identity Toolkit endpoint used by the Auth instance.
1063    pub fn with_identity_toolkit_endpoint(mut self, endpoint: impl Into<String>) -> Self {
1064        self.identity_toolkit_endpoint = Some(endpoint.into());
1065        self
1066    }
1067
1068    /// Overrides the Secure Token endpoint used for refresh operations.
1069    pub fn with_secure_token_endpoint(mut self, endpoint: impl Into<String>) -> Self {
1070        self.secure_token_endpoint = Some(endpoint.into());
1071        self
1072    }
1073
1074    /// Prevents `build` from automatically calling `initialize`.
1075    pub fn defer_initialization(mut self) -> Self {
1076        self.auto_initialize = false;
1077        self
1078    }
1079
1080    /// Builds the Auth instance, applying all configured overrides.
1081    pub fn build(self) -> AuthResult<Arc<Auth>> {
1082        let persistence = self
1083            .persistence
1084            .unwrap_or_else(|| Arc::new(InMemoryPersistence::default()));
1085        let auth = Arc::new(Auth::new_with_persistence(self.app, persistence)?);
1086        if let Some(handler) = self.popup_handler {
1087            auth.set_popup_handler(handler);
1088        }
1089        if let Some(handler) = self.redirect_handler {
1090            auth.set_redirect_handler(handler);
1091        }
1092        if let Some(request_uri) = self.oauth_request_uri {
1093            auth.set_oauth_request_uri(request_uri);
1094        }
1095        if let Some(persistence) = self.redirect_persistence {
1096            auth.set_redirect_persistence(persistence);
1097        }
1098        if let Some(endpoint) = self.identity_toolkit_endpoint {
1099            auth.set_identity_toolkit_endpoint(endpoint);
1100        }
1101        if let Some(endpoint) = self.secure_token_endpoint {
1102            auth.set_secure_token_endpoint(endpoint);
1103        }
1104        if self.auto_initialize {
1105            auth.initialize()?;
1106        }
1107        Ok(auth)
1108    }
1109}
1110
1111/// Registers the Auth component so apps can resolve `Auth` instances.
1112pub fn register_auth_component() {
1113    use std::sync::LazyLock;
1114    static REGISTERED: LazyLock<()> = LazyLock::new(|| {
1115        let component = Component::new("auth", Arc::new(auth_factory), ComponentType::Public)
1116            .with_instantiation_mode(InstantiationMode::Lazy);
1117        let _ = crate::component::register_component(component);
1118    });
1119    LazyLock::force(&REGISTERED);
1120}
1121
1122fn auth_factory(
1123    container: &ComponentContainer,
1124    _options: InstanceFactoryOptions,
1125) -> Result<DynService, ComponentError> {
1126    let app = container.root_service::<FirebaseApp>().ok_or_else(|| {
1127        ComponentError::InitializationFailed {
1128            name: "auth".to_string(),
1129            reason: "Firebase app not attached to component container".to_string(),
1130        }
1131    })?;
1132    let auth = Auth::new((*app).clone()).map_err(|err| ComponentError::InitializationFailed {
1133        name: "auth".to_string(),
1134        reason: err.to_string(),
1135    })?;
1136    let auth = Arc::new(auth);
1137    auth.initialize()
1138        .map_err(|err| ComponentError::InitializationFailed {
1139            name: "auth".to_string(),
1140            reason: err.to_string(),
1141        })?;
1142    Ok(auth as DynService)
1143}
1144
1145/// Retrieves the `Auth` service for the provided app, initializing if needed.
1146pub fn auth_for_app(app: FirebaseApp) -> AuthResult<Arc<Auth>> {
1147    let provider = app.container().get_provider("auth");
1148    provider.get_immediate::<Auth>().ok_or_else(|| {
1149        AuthError::App(AppError::ComponentFailure {
1150            component: "auth".to_string(),
1151            message: "Auth service not initialized".to_string(),
1152        })
1153    })
1154}
1155
1156fn sleep_with_cancel(mut duration: Duration, cancel_flag: &AtomicBool) -> bool {
1157    while duration > Duration::ZERO {
1158        if cancel_flag.load(Ordering::SeqCst) {
1159            return false;
1160        }
1161        let step = if duration > Duration::from_millis(250) {
1162            Duration::from_millis(250)
1163        } else {
1164            duration
1165        };
1166        thread::sleep(step);
1167        duration = duration.checked_sub(step).unwrap_or(Duration::ZERO);
1168    }
1169
1170    !cancel_flag.load(Ordering::SeqCst)
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175    use super::*;
1176    use crate::test_support::{start_mock_server, test_firebase_app_with_api_key};
1177    use httpmock::prelude::*;
1178    use serde_json::json;
1179
1180    const TEST_API_KEY: &str = "test-api-key";
1181    const TEST_EMAIL: &str = "user@example.com";
1182    const TEST_PASSWORD: &str = "secret";
1183    const TEST_UID: &str = "uid-123";
1184    const TEST_ID_TOKEN: &str = "id-token";
1185    const TEST_REFRESH_TOKEN: &str = "refresh-token";
1186    const REAUTH_EMAIL: &str = "reauth@example.com";
1187    const REAUTH_ID_TOKEN: &str = "reauth-id-token";
1188    const REAUTH_REFRESH_TOKEN: &str = "reauth-refresh-token";
1189    const REAUTH_UID: &str = "reauth-uid";
1190    const GOOGLE_PROVIDER_ID: &str = "google.com";
1191    const UPDATED_ID_TOKEN: &str = "updated-id-token";
1192    const UPDATED_REFRESH_TOKEN: &str = "updated-refresh-token";
1193
1194    fn build_auth(server: &MockServer) -> Arc<Auth> {
1195        Auth::builder(test_firebase_app_with_api_key(TEST_API_KEY))
1196            .with_identity_toolkit_endpoint(server.url("/v1"))
1197            .with_secure_token_endpoint(server.url("/token"))
1198            .defer_initialization()
1199            .build()
1200            .expect("failed to build auth")
1201    }
1202
1203    fn sign_in_user(auth: &Arc<Auth>, server: &MockServer) {
1204        let mock = server.mock(|when, then| {
1205            when.method(POST)
1206                .path("/v1/accounts:signInWithPassword")
1207                .query_param("key", TEST_API_KEY)
1208                .json_body(json!({
1209                    "email": TEST_EMAIL,
1210                    "password": TEST_PASSWORD,
1211                    "returnSecureToken": true
1212                }));
1213            then.status(200).json_body(json!({
1214                "localId": TEST_UID,
1215                "email": TEST_EMAIL,
1216                "idToken": TEST_ID_TOKEN,
1217                "refreshToken": TEST_REFRESH_TOKEN,
1218                "expiresIn": "3600"
1219            }));
1220        });
1221
1222        auth.sign_in_with_email_and_password(TEST_EMAIL, TEST_PASSWORD)
1223            .expect("sign-in should succeed");
1224        mock.assert();
1225    }
1226
1227    #[test]
1228    fn sign_in_with_email_and_password_success() {
1229        let server = start_mock_server();
1230        let auth = build_auth(&server);
1231
1232        let mock = server.mock(|when, then| {
1233            when.method(POST)
1234                .path("/v1/accounts:signInWithPassword")
1235                .query_param("key", TEST_API_KEY)
1236                .json_body(json!({
1237                    "email": "user@example.com",
1238                    "password": "secret",
1239                    "returnSecureToken": true
1240                }));
1241            then.status(200).json_body(json!({
1242                "localId": "uid-123",
1243                "email": "user@example.com",
1244                "idToken": "id-token",
1245                "refreshToken": "refresh-token",
1246                "expiresIn": "3600"
1247            }));
1248        });
1249
1250        let credential = auth
1251            .sign_in_with_email_and_password("user@example.com", "secret")
1252            .expect("sign-in should succeed");
1253
1254        mock.assert();
1255        assert_eq!(
1256            credential.provider_id.as_deref(),
1257            Some(EmailAuthProvider::PROVIDER_ID)
1258        );
1259        assert_eq!(credential.operation_type.as_deref(), Some("signIn"));
1260        assert_eq!(credential.user.uid(), "uid-123");
1261        assert_eq!(
1262            credential.user.token_manager().access_token(),
1263            Some("id-token".to_string())
1264        );
1265        assert_eq!(
1266            credential.user.refresh_token(),
1267            Some("refresh-token".to_string())
1268        );
1269    }
1270
1271    #[test]
1272    fn create_user_with_email_and_password_success() {
1273        let server = start_mock_server();
1274        let auth = build_auth(&server);
1275
1276        let mock = server.mock(|when, then| {
1277            when.method(POST)
1278                .path("/v1/accounts:signUp")
1279                .query_param("key", TEST_API_KEY)
1280                .json_body(json!({
1281                    "email": "user@example.com",
1282                    "password": "secret",
1283                    "returnSecureToken": true
1284                }));
1285            then.status(200).json_body(json!({
1286                "localId": "uid-456",
1287                "email": "user@example.com",
1288                "idToken": "new-id-token",
1289                "refreshToken": "new-refresh-token",
1290                "expiresIn": "7200"
1291            }));
1292        });
1293
1294        let credential = auth
1295            .create_user_with_email_and_password("user@example.com", "secret")
1296            .expect("sign-up should succeed");
1297
1298        mock.assert();
1299        assert_eq!(
1300            credential.provider_id.as_deref(),
1301            Some(EmailAuthProvider::PROVIDER_ID)
1302        );
1303        assert_eq!(credential.operation_type.as_deref(), Some("signUp"));
1304        assert_eq!(credential.user.uid(), "uid-456");
1305        assert_eq!(
1306            credential.user.token_manager().access_token(),
1307            Some("new-id-token".to_string())
1308        );
1309        assert_eq!(
1310            credential.user.refresh_token(),
1311            Some("new-refresh-token".to_string())
1312        );
1313    }
1314
1315    #[test]
1316    fn sign_in_with_invalid_expires_in_returns_error() {
1317        let server = start_mock_server();
1318        let auth = build_auth(&server);
1319
1320        let mock = server.mock(|when, then| {
1321            when.method(POST)
1322                .path("/v1/accounts:signInWithPassword")
1323                .query_param("key", TEST_API_KEY)
1324                .json_body(json!({
1325                    "email": "user@example.com",
1326                    "password": "secret",
1327                    "returnSecureToken": true
1328                }));
1329            then.status(200).json_body(json!({
1330                "localId": "uid-123",
1331                "email": "user@example.com",
1332                "idToken": "id-token",
1333                "refreshToken": "refresh-token",
1334                "expiresIn": "not-a-number"
1335            }));
1336        });
1337
1338        let result = auth.sign_in_with_email_and_password("user@example.com", "secret");
1339
1340        mock.assert();
1341        assert!(matches!(
1342            result,
1343            Err(AuthError::InvalidCredential(message)) if message.contains("Invalid expiresIn value")
1344        ));
1345    }
1346
1347    #[test]
1348    fn sign_in_propagates_http_errors() {
1349        let server = start_mock_server();
1350        let auth = build_auth(&server);
1351
1352        let mock = server.mock(|when, then| {
1353            when.method(POST)
1354                .path("/v1/accounts:signInWithPassword")
1355                .query_param("key", TEST_API_KEY);
1356            then.status(400)
1357                .body("{\"error\":{\"message\":\"INVALID_PASSWORD\"}}");
1358        });
1359
1360        let result = auth.sign_in_with_email_and_password("user@example.com", "wrong-password");
1361
1362        mock.assert();
1363        assert!(matches!(
1364            result,
1365            Err(AuthError::Network(message)) if message.contains("INVALID_PASSWORD")
1366        ));
1367    }
1368
1369    #[test]
1370    fn send_password_reset_email_sends_request_body() {
1371        let server = start_mock_server();
1372        let auth = build_auth(&server);
1373
1374        let mock = server.mock(|when, then| {
1375            when.method(POST)
1376                .path("/v1/accounts:sendOobCode")
1377                .query_param("key", TEST_API_KEY)
1378                .json_body(json!({
1379                    "requestType": "PASSWORD_RESET",
1380                    "email": TEST_EMAIL
1381                }));
1382            then.status(200);
1383        });
1384
1385        auth.send_password_reset_email(TEST_EMAIL)
1386            .expect("password reset should succeed");
1387
1388        mock.assert();
1389    }
1390
1391    #[test]
1392    fn send_email_verification_uses_current_user_token() {
1393        let server = start_mock_server();
1394        let auth = build_auth(&server);
1395        sign_in_user(&auth, &server);
1396
1397        let mock = server.mock(|when, then| {
1398            when.method(POST)
1399                .path("/v1/accounts:sendOobCode")
1400                .query_param("key", TEST_API_KEY)
1401                .json_body(json!({
1402                    "requestType": "VERIFY_EMAIL",
1403                    "idToken": TEST_ID_TOKEN
1404                }));
1405            then.status(200);
1406        });
1407
1408        auth.send_email_verification()
1409            .expect("email verification should succeed");
1410
1411        mock.assert();
1412    }
1413
1414    #[test]
1415    fn confirm_password_reset_posts_new_password() {
1416        let server = start_mock_server();
1417        let auth = build_auth(&server);
1418
1419        let mock = server.mock(|when, then| {
1420            when.method(POST)
1421                .path("/v1/accounts:resetPassword")
1422                .query_param("key", TEST_API_KEY)
1423                .json_body(json!({
1424                    "oobCode": "reset-code",
1425                    "newPassword": "new-secret"
1426                }));
1427            then.status(200);
1428        });
1429
1430        auth.confirm_password_reset("reset-code", "new-secret")
1431            .expect("confirm reset should succeed");
1432
1433        mock.assert();
1434    }
1435
1436    #[test]
1437    fn update_profile_sets_display_name() {
1438        let server = start_mock_server();
1439        let auth = build_auth(&server);
1440        sign_in_user(&auth, &server);
1441
1442        let mock = server.mock(|when, then| {
1443            when.method(POST)
1444                .path("/v1/accounts:update")
1445                .query_param("key", TEST_API_KEY)
1446                .json_body(json!({
1447                    "idToken": TEST_ID_TOKEN,
1448                    "displayName": "New Name",
1449                    "returnSecureToken": true
1450                }));
1451            then.status(200).json_body(json!({
1452                "idToken": TEST_ID_TOKEN,
1453                "refreshToken": TEST_REFRESH_TOKEN,
1454                "expiresIn": "3600",
1455                "localId": TEST_UID,
1456                "email": TEST_EMAIL,
1457                "displayName": "New Name"
1458            }));
1459        });
1460
1461        let user = auth
1462            .update_profile(Some("New Name"), None)
1463            .expect("update profile should succeed");
1464
1465        mock.assert();
1466        assert_eq!(user.info().display_name.as_deref(), Some("New Name"));
1467        assert_eq!(
1468            user.token_manager().access_token(),
1469            Some(TEST_ID_TOKEN.to_string())
1470        );
1471    }
1472
1473    #[test]
1474    fn update_profile_clears_display_name_when_empty_string() {
1475        let server = start_mock_server();
1476        let auth = build_auth(&server);
1477        sign_in_user(&auth, &server);
1478
1479        let set_mock = server.mock(|when, then| {
1480            when.method(POST)
1481                .path("/v1/accounts:update")
1482                .query_param("key", TEST_API_KEY)
1483                .json_body(json!({
1484                    "idToken": TEST_ID_TOKEN,
1485                    "displayName": "Existing",
1486                    "returnSecureToken": true
1487                }));
1488            then.status(200).json_body(json!({
1489                "idToken": TEST_ID_TOKEN,
1490                "refreshToken": TEST_REFRESH_TOKEN,
1491                "expiresIn": "3600",
1492                "localId": TEST_UID,
1493                "email": TEST_EMAIL,
1494                "displayName": "Existing"
1495            }));
1496        });
1497
1498        auth.update_profile(Some("Existing"), None)
1499            .expect("initial update should succeed");
1500        set_mock.assert();
1501
1502        let clear_mock = server.mock(|when, then| {
1503            when.method(POST)
1504                .path("/v1/accounts:update")
1505                .query_param("key", TEST_API_KEY)
1506                .json_body(json!({
1507                    "idToken": TEST_ID_TOKEN,
1508                    "deleteAttribute": ["DISPLAY_NAME"],
1509                    "returnSecureToken": true
1510                }));
1511            then.status(200).json_body(json!({
1512                "idToken": TEST_ID_TOKEN,
1513                "refreshToken": TEST_REFRESH_TOKEN,
1514                "expiresIn": "3600",
1515                "localId": TEST_UID,
1516                "email": TEST_EMAIL,
1517                "displayName": ""
1518            }));
1519        });
1520
1521        let user = auth
1522            .update_profile(Some(""), None)
1523            .expect("clear update should succeed");
1524
1525        clear_mock.assert();
1526        assert!(user.info().display_name.is_none());
1527    }
1528
1529    #[test]
1530    fn update_email_sets_new_email() {
1531        let server = start_mock_server();
1532        let auth = build_auth(&server);
1533        sign_in_user(&auth, &server);
1534
1535        let mock = server.mock(|when, then| {
1536            when.method(POST)
1537                .path("/v1/accounts:update")
1538                .query_param("key", TEST_API_KEY)
1539                .json_body(json!({
1540                    "idToken": TEST_ID_TOKEN,
1541                    "email": "new@example.com",
1542                    "returnSecureToken": true
1543                }));
1544            then.status(200).json_body(json!({
1545                "idToken": UPDATED_ID_TOKEN,
1546                "refreshToken": UPDATED_REFRESH_TOKEN,
1547                "expiresIn": "3600",
1548                "localId": TEST_UID,
1549                "email": "new@example.com"
1550            }));
1551        });
1552
1553        let user = auth
1554            .update_email("new@example.com")
1555            .expect("update email should succeed");
1556
1557        mock.assert();
1558        assert_eq!(user.info().email.as_deref(), Some("new@example.com"));
1559        assert_eq!(
1560            user.token_manager().access_token(),
1561            Some(UPDATED_ID_TOKEN.to_string())
1562        );
1563    }
1564
1565    #[test]
1566    fn update_password_refreshes_tokens() {
1567        let server = start_mock_server();
1568        let auth = build_auth(&server);
1569        sign_in_user(&auth, &server);
1570
1571        let mock = server.mock(|when, then| {
1572            when.method(POST)
1573                .path("/v1/accounts:update")
1574                .query_param("key", TEST_API_KEY)
1575                .json_body(json!({
1576                    "idToken": TEST_ID_TOKEN,
1577                    "password": "new-secret",
1578                    "returnSecureToken": true
1579                }));
1580            then.status(200).json_body(json!({
1581                "idToken": UPDATED_ID_TOKEN,
1582                "refreshToken": UPDATED_REFRESH_TOKEN,
1583                "expiresIn": "3600",
1584                "localId": TEST_UID,
1585                "email": TEST_EMAIL
1586            }));
1587        });
1588
1589        let user = auth
1590            .update_password("new-secret")
1591            .expect("update password should succeed");
1592
1593        mock.assert();
1594        assert_eq!(user.uid(), TEST_UID);
1595        assert_eq!(
1596            user.token_manager().refresh_token(),
1597            Some(UPDATED_REFRESH_TOKEN.to_string())
1598        );
1599    }
1600
1601    #[test]
1602    fn delete_user_clears_current_user_state() {
1603        let server = start_mock_server();
1604        let auth = build_auth(&server);
1605        sign_in_user(&auth, &server);
1606
1607        let mock = server.mock(|when, then| {
1608            when.method(POST)
1609                .path("/v1/accounts:delete")
1610                .query_param("key", TEST_API_KEY)
1611                .json_body(json!({
1612                    "idToken": TEST_ID_TOKEN
1613                }));
1614            then.status(200);
1615        });
1616
1617        auth.delete_user().expect("delete user should succeed");
1618
1619        mock.assert();
1620        assert!(auth.current_user().is_none());
1621    }
1622
1623    #[test]
1624    fn reauthenticate_with_password_updates_current_user() {
1625        let server = start_mock_server();
1626        let auth = build_auth(&server);
1627        sign_in_user(&auth, &server);
1628
1629        let mock = server.mock(|when, then| {
1630            when.method(POST)
1631                .path("/v1/accounts:signInWithPassword")
1632                .query_param("key", TEST_API_KEY)
1633                .json_body(json!({
1634                    "email": REAUTH_EMAIL,
1635                    "password": TEST_PASSWORD,
1636                    "returnSecureToken": true
1637                }));
1638            then.status(200).json_body(json!({
1639                "localId": REAUTH_UID,
1640                "email": REAUTH_EMAIL,
1641                "idToken": REAUTH_ID_TOKEN,
1642                "refreshToken": REAUTH_REFRESH_TOKEN,
1643                "expiresIn": "3600"
1644            }));
1645        });
1646
1647        let user = auth
1648            .reauthenticate_with_password(REAUTH_EMAIL, TEST_PASSWORD)
1649            .expect("reauth should succeed");
1650
1651        mock.assert();
1652        assert_eq!(user.uid(), REAUTH_UID);
1653        assert_eq!(user.info().email.as_deref(), Some(REAUTH_EMAIL));
1654        assert_eq!(
1655            user.token_manager().access_token(),
1656            Some(REAUTH_ID_TOKEN.to_string())
1657        );
1658        assert_eq!(
1659            auth.current_user().expect("current user set").uid(),
1660            REAUTH_UID
1661        );
1662    }
1663
1664    #[test]
1665    fn unlink_providers_sends_delete_provider() {
1666        let server = start_mock_server();
1667        let auth = build_auth(&server);
1668        sign_in_user(&auth, &server);
1669
1670        let mock = server.mock(|when, then| {
1671            when.method(POST)
1672                .path("/v1/accounts:update")
1673                .query_param("key", TEST_API_KEY)
1674                .json_body(json!({
1675                    "idToken": TEST_ID_TOKEN,
1676                    "deleteProvider": [GOOGLE_PROVIDER_ID],
1677                    "returnSecureToken": true
1678                }));
1679            then.status(200).json_body(json!({
1680                "idToken": TEST_ID_TOKEN,
1681                "refreshToken": TEST_REFRESH_TOKEN,
1682                "expiresIn": "3600",
1683                "localId": TEST_UID,
1684                "email": TEST_EMAIL,
1685                "providerUserInfo": []
1686            }));
1687        });
1688
1689        let user = auth
1690            .unlink_providers(&[GOOGLE_PROVIDER_ID])
1691            .expect("unlink should succeed");
1692
1693        mock.assert();
1694        assert_eq!(user.uid(), TEST_UID);
1695        assert_eq!(user.info().provider_id, EmailAuthProvider::PROVIDER_ID);
1696    }
1697
1698    #[test]
1699    fn unlink_providers_propagates_errors() {
1700        let server = start_mock_server();
1701        let auth = build_auth(&server);
1702        sign_in_user(&auth, &server);
1703
1704        let mock = server.mock(|when, then| {
1705            when.method(POST)
1706                .path("/v1/accounts:update")
1707                .query_param("key", TEST_API_KEY);
1708            then.status(400)
1709                .body("{\"error\":{\"message\":\"INVALID_PROVIDER_ID\"}}");
1710        });
1711
1712        let result = auth.unlink_providers(&[GOOGLE_PROVIDER_ID]);
1713
1714        mock.assert();
1715        assert!(matches!(
1716            result,
1717            Err(AuthError::InvalidCredential(message)) if message == "INVALID_PROVIDER_ID"
1718        ));
1719    }
1720
1721    #[test]
1722    fn get_account_info_returns_users() {
1723        let server = start_mock_server();
1724        let auth = build_auth(&server);
1725        sign_in_user(&auth, &server);
1726
1727        let mock = server.mock(|when, then| {
1728            when.method(POST)
1729                .path("/v1/accounts:lookup")
1730                .query_param("key", TEST_API_KEY)
1731                .json_body(json!({
1732                    "idToken": TEST_ID_TOKEN
1733                }));
1734            then.status(200).json_body(json!({
1735                "users": [
1736                    {
1737                        "localId": TEST_UID,
1738                        "displayName": "my-name",
1739                        "email": TEST_EMAIL,
1740                        "providerUserInfo": [
1741                            {
1742                                "providerId": GOOGLE_PROVIDER_ID,
1743                                "email": TEST_EMAIL
1744                            }
1745                        ]
1746                    }
1747                ]
1748            }));
1749        });
1750
1751        let response = auth
1752            .get_account_info()
1753            .expect("get account info should succeed");
1754
1755        mock.assert();
1756        assert_eq!(response.users.len(), 1);
1757        assert_eq!(response.users[0].display_name.as_deref(), Some("my-name"));
1758        assert_eq!(response.users[0].email.as_deref(), Some(TEST_EMAIL));
1759        let providers = response.users[0]
1760            .provider_user_info
1761            .as_ref()
1762            .expect("providers present");
1763        assert_eq!(providers.len(), 1);
1764        assert_eq!(
1765            providers[0].provider_id.as_deref(),
1766            Some(GOOGLE_PROVIDER_ID)
1767        );
1768    }
1769
1770    #[test]
1771    fn get_account_info_propagates_errors() {
1772        let server = start_mock_server();
1773        let auth = build_auth(&server);
1774        sign_in_user(&auth, &server);
1775
1776        let mock = server.mock(|when, then| {
1777            when.method(POST)
1778                .path("/v1/accounts:lookup")
1779                .query_param("key", TEST_API_KEY);
1780            then.status(400)
1781                .body("{\"error\":{\"message\":\"INVALID_ID_TOKEN\"}}");
1782        });
1783
1784        let result = auth.get_account_info();
1785
1786        mock.assert();
1787        assert!(matches!(
1788            result,
1789            Err(AuthError::InvalidCredential(message)) if message == "INVALID_ID_TOKEN"
1790        ));
1791    }
1792}