rs_firebase_admin_sdk/auth/
mod.rs

1//! Firebase Identity Provider management interface
2
3#[cfg(test)]
4mod test;
5
6pub mod claims;
7pub mod import;
8pub mod oob_code;
9
10#[cfg(feature = "tokens")]
11pub mod token;
12
13use crate::api_uri::{ApiUriBuilder, FirebaseAuthEmulatorRestApi, FirebaseAuthRestApi};
14use crate::client::error::ApiClientError;
15use crate::client::ApiHttpClient;
16use crate::util::{I128EpochMs, StrEpochMs, StrEpochSec};
17pub use claims::Claims;
18use error_stack::Report;
19use http::Method;
20pub use import::{UserImportRecord, UserImportRecords};
21use oob_code::{OobCodeAction, OobCodeActionLink, OobCodeActionType};
22use serde::{Deserialize, Serialize};
23use std::collections::BTreeMap;
24use std::future::Future;
25use std::vec;
26use time::{Duration, OffsetDateTime};
27
28const FIREBASE_AUTH_REST_AUTHORITY: &str = "identitytoolkit.googleapis.com";
29
30const FIREBASE_AUTH_SCOPES: [&str; 2] = [
31    "https://www.googleapis.com/auth/cloud-platform",
32    "https://www.googleapis.com/auth/userinfo.email",
33];
34
35#[derive(Serialize, Debug, Clone, Default)]
36#[serde(rename_all = "camelCase")]
37pub struct NewUser {
38    #[serde(skip_serializing_if = "Option::is_none")]
39    #[serde(rename = "localId")]
40    pub uid: Option<String>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub email: Option<String>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub password: Option<String>,
45}
46
47impl NewUser {
48    pub fn email_and_password(email: String, password: String) -> Self {
49        Self {
50            uid: None,
51            email: Some(email),
52            password: Some(password),
53        }
54    }
55}
56
57#[derive(Deserialize, Debug, Clone)]
58#[serde(rename_all = "camelCase")]
59pub struct ProviderUserInfo {
60    pub provider_id: String,
61    pub email: Option<String>,
62    pub phone_number: Option<String>,
63    pub federated_id: Option<String>,
64    pub raw_id: String,
65}
66
67#[derive(Deserialize, Debug, Clone)]
68#[serde(rename_all = "camelCase")]
69pub struct User {
70    #[serde(rename = "localId")]
71    pub uid: String,
72    pub email: Option<String>,
73    pub display_name: Option<String>,
74    pub photo_url: Option<String>,
75    pub phone_number: Option<String>,
76    pub last_login_at: Option<StrEpochMs>,
77    pub email_verified: Option<bool>,
78    pub password_updated_at: Option<I128EpochMs>,
79    pub valid_since: Option<StrEpochSec>,
80    pub created_at: Option<StrEpochMs>,
81    pub salt: Option<String>,
82    pub password_hash: Option<String>,
83    pub provider_user_info: Option<Vec<ProviderUserInfo>>,
84    #[serde(rename = "customAttributes")]
85    pub custom_claims: Option<Claims>,
86    pub disabled: Option<bool>,
87}
88
89#[derive(Deserialize, Debug, Clone)]
90#[serde(rename_all = "camelCase")]
91pub struct Users {
92    pub users: Option<Vec<User>>,
93}
94
95#[derive(Deserialize, Debug, Clone)]
96#[serde(rename_all = "camelCase")]
97pub struct UserList {
98    #[serde(default)]
99    pub users: Vec<User>,
100    pub next_page_token: Option<String>,
101}
102
103#[derive(Serialize, Debug, Clone)]
104#[serde(rename_all = "camelCase")]
105pub struct CreateSessionCookie {
106    pub id_token: String,
107    pub valid_duration: i64,
108}
109
110#[derive(Deserialize, Debug, Clone)]
111#[serde(rename_all = "camelCase")]
112pub struct SessionCookie {
113    pub session_cookie: String,
114}
115
116#[derive(Serialize, Debug, Clone)]
117#[serde(rename_all = "camelCase")]
118pub struct FederatedUserId {
119    pub provider_id: String,
120    pub raw_id: String,
121}
122
123#[derive(Serialize, Debug, Clone, Default)]
124#[serde(rename_all = "camelCase")]
125pub struct UserIdentifiers {
126    #[serde(skip_serializing_if = "Option::is_none")]
127    #[serde(rename = "localId")]
128    pub uid: Option<Vec<String>>,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub email: Option<Vec<String>>,
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub phone_number: Option<Vec<String>>,
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub federated_user_id: Option<FederatedUserId>,
135}
136
137impl UserIdentifiers {
138    pub fn builder() -> UserIdentifiersBuilder {
139        UserIdentifiersBuilder::default()
140    }
141}
142
143#[derive(Clone, Default)]
144pub struct UserIdentifiersBuilder {
145    ids: UserIdentifiers,
146}
147
148impl UserIdentifiersBuilder {
149    pub fn with_email(mut self, email: String) -> Self {
150        match &mut self.ids.email {
151            Some(email_vec) => email_vec.push(email),
152            None => self.ids.email = Some(vec![email]),
153        };
154
155        self
156    }
157
158    pub fn with_uid(mut self, uid: String) -> Self {
159        match &mut self.ids.uid {
160            Some(uid_vec) => uid_vec.push(uid),
161            None => self.ids.uid = Some(vec![uid]),
162        };
163
164        self
165    }
166
167    pub fn with_phone_number(mut self, pnumber: String) -> Self {
168        match &mut self.ids.phone_number {
169            Some(pnumber_vec) => pnumber_vec.push(pnumber),
170            None => self.ids.phone_number = Some(vec![pnumber]),
171        };
172
173        self
174    }
175
176    pub fn build(self) -> UserIdentifiers {
177        self.ids
178    }
179}
180
181#[derive(Serialize, Debug, Clone)]
182pub enum DeleteAttribute {
183    #[serde(rename = "DISPLAY_NAME")]
184    DisplayName,
185    #[serde(rename = "PHOTO_URL")]
186    PhotoUrl,
187}
188
189#[derive(Serialize, Debug, Clone)]
190#[serde(rename_all = "camelCase")]
191pub enum DeleteProvider {
192    Phone,
193}
194
195#[derive(Serialize, Debug, Clone, Default)]
196#[serde(rename_all = "camelCase")]
197pub struct UserUpdate {
198    #[serde(rename = "localId")]
199    pub uid: String,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub email: Option<String>,
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub password: Option<String>,
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub valid_since: Option<OffsetDateTime>,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub email_verified: Option<bool>,
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub disable_user: Option<bool>,
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub display_name: Option<String>,
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub photo_url: Option<String>,
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub phone_number: Option<String>,
216    #[serde(rename = "customAttributes")]
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub custom_claims: Option<Claims>,
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub delete_attribute: Option<Vec<DeleteAttribute>>,
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub delete_provider: Option<Vec<DeleteProvider>>,
223}
224
225impl UserUpdate {
226    pub fn builder(uid: String) -> UserUpdateBuilder {
227        UserUpdateBuilder::new(uid)
228    }
229}
230
231pub struct UserUpdateBuilder {
232    update: UserUpdate,
233}
234
235pub enum AttributeOp<T> {
236    Change(T),
237    Delete,
238}
239
240impl UserUpdateBuilder {
241    pub fn new(uid: String) -> Self {
242        Self {
243            update: UserUpdate {
244                uid,
245                ..Default::default()
246            },
247        }
248    }
249
250    pub fn display_name(mut self, value: AttributeOp<String>) -> Self {
251        match value {
252            AttributeOp::Change(new_display_name) => {
253                self.update.display_name = Some(new_display_name)
254            }
255            AttributeOp::Delete => self
256                .update
257                .delete_attribute
258                .get_or_insert(Vec::new())
259                .push(DeleteAttribute::DisplayName),
260        };
261
262        self
263    }
264
265    pub fn photo_url(mut self, value: AttributeOp<String>) -> Self {
266        match value {
267            AttributeOp::Change(new_photo_url) => self.update.photo_url = Some(new_photo_url),
268            AttributeOp::Delete => self
269                .update
270                .delete_attribute
271                .get_or_insert(Vec::new())
272                .push(DeleteAttribute::PhotoUrl),
273        };
274
275        self
276    }
277
278    pub fn phone_number(mut self, value: AttributeOp<String>) -> Self {
279        match value {
280            AttributeOp::Change(new_phone_number) => {
281                self.update.phone_number = Some(new_phone_number)
282            }
283            AttributeOp::Delete => self
284                .update
285                .delete_provider
286                .get_or_insert(Vec::new())
287                .push(DeleteProvider::Phone),
288        };
289
290        self
291    }
292
293    pub fn custom_claims(mut self, value: Claims) -> Self {
294        self.update.custom_claims = Some(value);
295
296        self
297    }
298
299    pub fn email(mut self, value: String) -> Self {
300        self.update.email = Some(value);
301
302        self
303    }
304
305    pub fn password(mut self, value: String) -> Self {
306        self.update.password = Some(value);
307
308        self
309    }
310
311    pub fn email_verified(mut self, value: bool) -> Self {
312        self.update.email_verified = Some(value);
313
314        self
315    }
316
317    pub fn disabled(mut self, is_disabled: bool) -> Self {
318        self.update.disable_user = Some(is_disabled);
319
320        self
321    }
322
323    pub fn build(self) -> UserUpdate {
324        self.update
325    }
326}
327
328#[derive(Serialize, Debug, Clone, Default)]
329#[serde(rename_all = "camelCase")]
330struct UserId {
331    #[serde(rename = "localId")]
332    pub uid: String,
333}
334
335#[derive(Serialize, Debug, Clone, Default)]
336#[serde(rename_all = "camelCase")]
337struct UserIds {
338    #[serde(rename = "localIds")]
339    pub uids: Vec<String>,
340    pub force: bool,
341}
342
343pub trait FirebaseAuthService<C: ApiHttpClient>: Send + Sync + 'static {
344    fn get_client(&self) -> &C;
345    fn get_auth_uri_builder(&self) -> &ApiUriBuilder;
346
347    /// Creates a new user account with the specified properties.
348    /// # Example
349    /// ```rust
350    /// let new_user = auth.create_user(
351    ///     NewUser::email_and_password(
352    ///        "test@example.com".into(),
353    ///        "123ABC".into(),
354    ///     )
355    /// ).await.unwrap();
356    /// ```
357    fn create_user(
358        &self,
359        user: NewUser,
360    ) -> impl Future<Output = Result<User, Report<ApiClientError>>> + Send {
361        let client = self.get_client();
362        let uri = self
363            .get_auth_uri_builder()
364            .build(FirebaseAuthRestApi::CreateUser);
365
366        client.send_request_body(uri, Method::POST, user, &FIREBASE_AUTH_SCOPES)
367    }
368
369    /// Get first user that matches given identifier filter
370    /// # Example
371    /// ```rust
372    /// let user = auth.get_user(
373    ///     UserIdentifiers {
374    ///         email: Some(vec!["me@example.com".into()]),
375    ///         ..Default::default()
376    ///     }
377    /// ).await.unwrap();
378    /// ```
379    fn get_user(
380        &self,
381        indentifiers: UserIdentifiers,
382    ) -> impl Future<Output = Result<Option<User>, Report<ApiClientError>>> + Send {
383        async move {
384            if let Some(users) = self.get_users(indentifiers).await? {
385                return Ok(users.into_iter().next());
386            }
387
388            Ok(None)
389        }
390    }
391
392    /// Get all users that match a given identifier filter
393    /// # Example
394    /// ```rust
395    /// let users = auth.get_users(
396    ///     UserIdentifiers {
397    ///         email: Some(vec!["me@example.com".into()]),
398    ///         uid: Some(vec!["A123456".into()]),
399    ///         ..Default::default()
400    ///     }
401    /// ).await.unwrap().unwrap();
402    /// ```
403    fn get_users(
404        &self,
405        indentifiers: UserIdentifiers,
406    ) -> impl Future<Output = Result<Option<Vec<User>>, Report<ApiClientError>>> + Send {
407        async move {
408            let client = self.get_client();
409            let uri_builder = self.get_auth_uri_builder();
410
411            let users: Users = client
412                .send_request_body(
413                    uri_builder.build(FirebaseAuthRestApi::GetUsers),
414                    Method::POST,
415                    indentifiers,
416                    &FIREBASE_AUTH_SCOPES,
417                )
418                .await?;
419
420            Ok(users.users)
421        }
422    }
423
424    /// Fetch all users in batches of `users_per_page`, to progress pass previous page into the method's `prev`.
425    /// # Example
426    /// ```rust
427    /// let mut user_page: Option<UserList> = None;
428    /// loop {
429    ///     user_page = auth.list_users(10, user_page).await.unwrap();
430    ///
431    ///     if let Some(user_page) = &user_page {
432    ///         for user in &user_page.users {
433    ///             println!("User: {user:?}");
434    ///         }
435    ///     } else {
436    ///         break;
437    ///     }
438    /// }
439    /// ```
440    fn list_users(
441        &self,
442        users_per_page: usize,
443        prev: Option<UserList>,
444    ) -> impl Future<Output = Result<Option<UserList>, Report<ApiClientError>>> + Send {
445        async move {
446            let client = self.get_client();
447            let uri_builder = self.get_auth_uri_builder();
448            let mut params = vec![("maxResults".to_string(), users_per_page.clone().to_string())];
449
450            if let Some(prev) = prev {
451                if let Some(next_page_token) = prev.next_page_token {
452                    params.push(("nextPageToken".to_string(), next_page_token));
453                } else {
454                    return Ok(None);
455                }
456            }
457
458            let users: UserList = client
459                .send_request_with_params(
460                    uri_builder.build(FirebaseAuthRestApi::ListUsers),
461                    params.into_iter(),
462                    Method::GET,
463                    &FIREBASE_AUTH_SCOPES,
464                )
465                .await?;
466
467            Ok(Some(users))
468        }
469    }
470
471    /// Delete user with given ID
472    fn delete_user(
473        &self,
474        uid: String,
475    ) -> impl Future<Output = Result<(), Report<ApiClientError>>> + Send {
476        async move {
477            let client = self.get_client();
478            let uri_builder = self.get_auth_uri_builder();
479
480            client
481                .send_request_body_empty_response(
482                    uri_builder.build(FirebaseAuthRestApi::DeleteUser),
483                    Method::POST,
484                    UserId { uid },
485                    &FIREBASE_AUTH_SCOPES,
486                )
487                .await
488        }
489    }
490
491    /// Delete all users with given list of IDs
492    fn delete_users(
493        &self,
494        uids: Vec<String>,
495        force: bool,
496    ) -> impl Future<Output = Result<(), Report<ApiClientError>>> + Send {
497        async move {
498            let client = self.get_client();
499            let uri_builder = self.get_auth_uri_builder();
500
501            client
502                .send_request_body_empty_response(
503                    uri_builder.build(FirebaseAuthRestApi::DeleteUsers),
504                    Method::POST,
505                    UserIds { uids, force },
506                    &FIREBASE_AUTH_SCOPES,
507                )
508                .await
509        }
510    }
511
512    /// Update user with given changes
513    /// # Example
514    /// ```rust
515    /// let update = UserUpdate::builder("ID123".into())
516    ///     .display_name(AttributeOp::Change("My new name".into()))
517    ///     .phone_number(AttributeOp::Delete)
518    ///     .email("new@example.com".into())
519    ///     .build();
520    /// auth.update_user(update).await.unwrap();
521    /// ```
522    fn update_user(
523        &self,
524        update: UserUpdate,
525    ) -> impl Future<Output = Result<User, Report<ApiClientError>>> + Send {
526        async move {
527            let client = self.get_client();
528            let uri_builder = self.get_auth_uri_builder();
529
530            client
531                .send_request_body(
532                    uri_builder.build(FirebaseAuthRestApi::UpdateUser),
533                    Method::POST,
534                    update,
535                    &FIREBASE_AUTH_SCOPES,
536                )
537                .await
538        }
539    }
540
541    /// Create users in bulk
542    /// # Example
543    /// ```rust
544    /// let records = vec![
545    ///     UserImportRecord::builder()
546    ///         .with_email("me@example.com".into(), true)
547    ///         .with_display_name("My Name".into())
548    ///         .build()
549    /// ];
550    /// auth.import_users(records).await.unwrap();
551    /// ```
552    fn import_users(
553        &self,
554        users: Vec<UserImportRecord>,
555    ) -> impl Future<Output = Result<(), Report<ApiClientError>>> + Send {
556        async move {
557            let client = self.get_client();
558            let uri_builder = self.get_auth_uri_builder();
559
560            client
561                .send_request_body_empty_response(
562                    uri_builder.build(FirebaseAuthRestApi::ImportUsers),
563                    Method::POST,
564                    UserImportRecords { users },
565                    &FIREBASE_AUTH_SCOPES,
566                )
567                .await?;
568
569            Ok(())
570        }
571    }
572
573    /// Send email with OOB code action
574    /// # Example
575    /// ```rust
576    /// let oob_action = OobCodeAction::builder(
577    ///     OobCodeActionType::PasswordReset,
578    ///     "me@example.com".into()
579    /// ).build();
580    ///
581    /// let link = auth.generate_email_action_link(oob_action).await.unwrap();
582    /// ```
583    fn generate_email_action_link(
584        &self,
585        oob_action: OobCodeAction,
586    ) -> impl Future<Output = Result<String, Report<ApiClientError>>> + Send {
587        async move {
588            let client = self.get_client();
589            let uri_builder = self.get_auth_uri_builder();
590
591            let oob_link: OobCodeActionLink = client
592                .send_request_body(
593                    uri_builder.build(FirebaseAuthRestApi::SendOobCode),
594                    Method::POST,
595                    oob_action,
596                    &FIREBASE_AUTH_SCOPES,
597                )
598                .await?;
599
600            Ok(oob_link.oob_link)
601        }
602    }
603
604    /// Create session cookie
605    /// that then can be verified and parsed with `App::live().cookie_token_verifier()`
606    fn create_session_cookie(
607        &self,
608        id_token: String,
609        expires_in: Duration,
610    ) -> impl Future<Output = Result<String, Report<ApiClientError>>> + Send {
611        async move {
612            let client = self.get_client();
613            let uri_builder = self.get_auth_uri_builder();
614
615            let create_cookie = CreateSessionCookie {
616                id_token,
617                valid_duration: expires_in.whole_seconds(),
618            };
619
620            let session_cookie: SessionCookie = client
621                .send_request_body(
622                    uri_builder.build(FirebaseAuthRestApi::CreateSessionCookie),
623                    Method::POST,
624                    create_cookie,
625                    &FIREBASE_AUTH_SCOPES,
626                )
627                .await?;
628
629            Ok(session_cookie.session_cookie)
630        }
631    }
632}
633
634#[derive(Serialize, Deserialize, Debug, Clone)]
635#[serde(rename_all = "camelCase")]
636pub struct EmulatorConfigurationSignIn {
637    allow_duplicate_emails: bool,
638}
639
640#[derive(Serialize, Deserialize, Debug, Clone)]
641#[serde(rename_all = "camelCase")]
642pub struct EmulatorConfiguration {
643    sign_in: EmulatorConfigurationSignIn,
644}
645
646#[derive(Deserialize, Debug, Clone)]
647#[serde(rename_all = "camelCase")]
648pub struct OobCode {
649    pub email: String,
650    pub oob_code: String,
651    pub oob_link: String,
652    pub request_type: OobCodeActionType,
653}
654
655#[derive(Deserialize, Debug, Clone)]
656#[serde(rename_all = "camelCase")]
657pub struct OobCodes {
658    pub oob_codes: Vec<OobCode>,
659}
660
661#[derive(Deserialize, Debug, Clone)]
662#[serde(rename_all = "camelCase")]
663pub struct SmsVerificationCode {
664    pub phone_number: String,
665    pub session_code: String,
666}
667
668#[derive(Deserialize, Debug, Clone)]
669#[serde(rename_all = "camelCase")]
670pub struct SmsVerificationCodes {
671    pub verification_codes: Vec<SmsVerificationCode>,
672}
673
674pub trait FirebaseEmulatorAuthService<ApiHttpClientT>
675where
676    Self: Send + Sync,
677    ApiHttpClientT: ApiHttpClient + Send + Sync,
678{
679    fn get_emulator_client(&self) -> &ApiHttpClientT;
680    fn get_emulator_auth_uri_builder(&self) -> &ApiUriBuilder;
681
682    /// Delete all users within emulator
683    fn clear_all_users(&self) -> impl Future<Output = Result<(), Report<ApiClientError>>> + Send {
684        async move {
685            let client = self.get_emulator_client();
686            let uri_builder = self.get_emulator_auth_uri_builder();
687
688            let _result: BTreeMap<String, String> = client
689                .send_request(
690                    uri_builder.build(FirebaseAuthEmulatorRestApi::ClearUserAccounts),
691                    Method::DELETE,
692                    &FIREBASE_AUTH_SCOPES,
693                )
694                .await?;
695
696            Ok(())
697        }
698    }
699
700    /// Get current emulator configuration
701    fn get_emulator_configuration(
702        &self,
703    ) -> impl Future<Output = Result<EmulatorConfiguration, Report<ApiClientError>>> + Send {
704        async move {
705            let client = self.get_emulator_client();
706            let uri_builder = self.get_emulator_auth_uri_builder();
707
708            client
709                .send_request(
710                    uri_builder.build(FirebaseAuthEmulatorRestApi::Configuration),
711                    Method::GET,
712                    &FIREBASE_AUTH_SCOPES,
713                )
714                .await
715        }
716    }
717
718    /// Update emulator configuration
719    fn patch_emulator_configuration(
720        &self,
721        configuration: EmulatorConfiguration,
722    ) -> impl Future<Output = Result<EmulatorConfiguration, Report<ApiClientError>>> + Send {
723        async move {
724            let client = self.get_emulator_client();
725            let uri_builder = self.get_emulator_auth_uri_builder();
726
727            client
728                .send_request_body(
729                    uri_builder.build(FirebaseAuthEmulatorRestApi::Configuration),
730                    Method::PATCH,
731                    configuration,
732                    &FIREBASE_AUTH_SCOPES,
733                )
734                .await
735        }
736    }
737
738    /// Fetch all OOB codes within emulator
739    fn get_oob_codes(
740        &self,
741    ) -> impl Future<Output = Result<Vec<OobCode>, Report<ApiClientError>>> + Send {
742        async move {
743            let client = self.get_emulator_client();
744            let uri_builder = self.get_emulator_auth_uri_builder();
745
746            let oob_codes: OobCodes = client
747                .send_request(
748                    uri_builder.build(FirebaseAuthEmulatorRestApi::OobCodes),
749                    Method::GET,
750                    &FIREBASE_AUTH_SCOPES,
751                )
752                .await?;
753
754            Ok(oob_codes.oob_codes)
755        }
756    }
757
758    /// Fetch all SMS codes within emulator
759    fn get_sms_verification_codes(
760        &self,
761    ) -> impl Future<Output = Result<SmsVerificationCodes, Report<ApiClientError>>> + Send {
762        async move {
763            let client = self.get_emulator_client();
764            let uri_builder = self.get_emulator_auth_uri_builder();
765
766            client
767                .send_request(
768                    uri_builder.build(FirebaseAuthEmulatorRestApi::SmsVerificationCodes),
769                    Method::GET,
770                    &FIREBASE_AUTH_SCOPES,
771                )
772                .await
773        }
774    }
775}
776
777pub struct FirebaseAuth<ApiHttpClientT> {
778    client: ApiHttpClientT,
779    auth_uri_builder: ApiUriBuilder,
780    emulator_auth_uri_builder: Option<ApiUriBuilder>,
781}
782
783impl<ApiHttpClientT> FirebaseAuth<ApiHttpClientT>
784where
785    ApiHttpClientT: ApiHttpClient + Send + Sync,
786{
787    /// Create Firebase Authentication manager for emulator
788    pub fn emulated(emulator_url: String, project_id: &str, client: ApiHttpClientT) -> Self {
789        let fb_auth_root = emulator_url.clone()
790            + &format!("/{FIREBASE_AUTH_REST_AUTHORITY}/v1/projects/{project_id}");
791        let fb_emu_root = emulator_url + &format!("/emulator/v1/projects/{project_id}");
792
793        Self {
794            client,
795            auth_uri_builder: ApiUriBuilder::new(fb_auth_root),
796            emulator_auth_uri_builder: Some(ApiUriBuilder::new(fb_emu_root)),
797        }
798    }
799
800    /// Create Firebase Authentication manager for live project
801    pub fn live(project_id: &str, client: ApiHttpClientT) -> Self {
802        let fb_auth_root = "https://".to_string()
803            + FIREBASE_AUTH_REST_AUTHORITY
804            + &format!("/v1/projects/{project_id}");
805
806        Self {
807            client,
808            auth_uri_builder: ApiUriBuilder::new(fb_auth_root),
809            emulator_auth_uri_builder: None,
810        }
811    }
812}
813
814impl<ApiHttpClientT> FirebaseAuthService<ApiHttpClientT> for FirebaseAuth<ApiHttpClientT>
815where
816    ApiHttpClientT: ApiHttpClient + Send + Sync,
817{
818    fn get_client(&self) -> &ApiHttpClientT {
819        &self.client
820    }
821
822    fn get_auth_uri_builder(&self) -> &ApiUriBuilder {
823        &self.auth_uri_builder
824    }
825}
826
827impl<ApiHttpClientT> FirebaseEmulatorAuthService<ApiHttpClientT> for FirebaseAuth<ApiHttpClientT>
828where
829    ApiHttpClientT: ApiHttpClient + Send + Sync,
830{
831    fn get_emulator_client(&self) -> &ApiHttpClientT {
832        &self.client
833    }
834
835    fn get_emulator_auth_uri_builder(&self) -> &ApiUriBuilder {
836        self.emulator_auth_uri_builder
837            .as_ref()
838            .expect("Auth emulator URI builder is unset")
839    }
840}