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 pub fn builder(app: FirebaseApp) -> AuthBuilder {
70 AuthBuilder::new(app)
71 }
72
73 pub fn new(app: FirebaseApp) -> AuthResult<Self> {
75 Self::new_with_persistence(app, Arc::new(InMemoryPersistence::default()))
76 }
77
78 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 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 pub fn app(&self) -> &FirebaseApp {
126 &self.app
127 }
128
129 pub fn current_user(&self) -> Option<Arc<User>> {
131 self.current_user.lock().unwrap().clone()
132 }
133
134 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 pub fn email_auth_provider(&self) -> EmailAuthProvider {
144 EmailAuthProvider
145 }
146
147 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 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 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 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 pub fn token_provider(self: &Arc<Self>) -> TokenProviderArc {
346 crate::auth::token_provider::auth_token_provider_arc(self.clone())
347 }
348
349 pub fn set_oauth_request_uri(&self, value: impl Into<String>) {
351 *self.oauth_request_uri.lock().unwrap() = value.into();
352 }
353
354 pub fn oauth_request_uri(&self) -> String {
356 self.oauth_request_uri.lock().unwrap().clone()
357 }
358
359 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 pub fn identity_toolkit_endpoint(&self) -> String {
368 self.identity_toolkit_endpoint.lock().unwrap().clone()
369 }
370
371 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 pub fn set_popup_handler(&self, handler: Arc<dyn OAuthPopupHandler>) {
384 *self.popup_handler.lock().unwrap() = Some(handler);
385 }
386
387 pub fn clear_popup_handler(&self) {
389 *self.popup_handler.lock().unwrap() = None;
390 }
391
392 pub fn popup_handler(&self) -> Option<Arc<dyn OAuthPopupHandler>> {
394 self.popup_handler.lock().unwrap().clone()
395 }
396
397 pub fn set_redirect_handler(&self, handler: Arc<dyn OAuthRedirectHandler>) {
399 *self.redirect_handler.lock().unwrap() = Some(handler);
400 }
401
402 pub fn clear_redirect_handler(&self) {
404 *self.redirect_handler.lock().unwrap() = None;
405 }
406
407 pub fn redirect_handler(&self) -> Option<Arc<dyn OAuthRedirectHandler>> {
409 self.redirect_handler.lock().unwrap().clone()
410 }
411
412 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 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 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 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 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 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 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 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 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 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 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 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 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 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(¤t_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 pub fn with_persistence(mut self, persistence: Arc<dyn AuthPersistence + Send + Sync>) -> Self {
1034 self.persistence = Some(persistence);
1035 self
1036 }
1037
1038 pub fn with_popup_handler(mut self, handler: Arc<dyn OAuthPopupHandler>) -> Self {
1040 self.popup_handler = Some(handler);
1041 self
1042 }
1043
1044 pub fn with_redirect_handler(mut self, handler: Arc<dyn OAuthRedirectHandler>) -> Self {
1046 self.redirect_handler = Some(handler);
1047 self
1048 }
1049
1050 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 pub fn with_redirect_persistence(mut self, persistence: Arc<dyn RedirectPersistence>) -> Self {
1058 self.redirect_persistence = Some(persistence);
1059 self
1060 }
1061
1062 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 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 pub fn defer_initialization(mut self) -> Self {
1076 self.auto_initialize = false;
1077 self
1078 }
1079
1080 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
1111pub 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(®ISTERED);
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
1145pub 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}