1#[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}