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