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