1pub mod keys;
8pub mod models;
9pub mod project_config;
10pub mod project_config_impl;
11pub mod tenant_mgt;
12pub mod verifier;
13
14use crate::auth::models::{
15 ActionCodeSettings, CreateSessionCookieRequest, CreateSessionCookieResponse, CreateUserRequest,
16 DeleteAccountRequest, EmailLinkRequest, EmailLinkResponse, GetAccountInfoRequest,
17 GetAccountInfoResponse, ImportUsersRequest, ImportUsersResponse, ListUsersResponse,
18 UpdateUserRequest, UserRecord,
19};
20use crate::auth::project_config_impl::ProjectConfig;
21use crate::auth::tenant_mgt::TenantAwareness;
22use crate::auth::verifier::{FirebaseTokenClaims, IdTokenVerifier, TokenVerificationError};
23use crate::core::middleware::AuthMiddleware;
24use crate::core::parse_error_response;
25use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
26use reqwest::header;
27use reqwest::Client;
28use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
29use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
30use serde::Serialize;
31use std::sync::Arc;
32use std::time::{SystemTime, UNIX_EPOCH};
33use thiserror::Error;
34use url::Url;
35
36const AUTH_V1_API: &str = "https://identitytoolkit.googleapis.com/v1/projects/{project_id}";
37const AUTH_V1_TENANT_API: &str =
38 "https://identitytoolkit.googleapis.com/v1/projects/{project_id}/tenants/{tenant_id}";
39
40#[derive(Error, Debug)]
42pub enum AuthError {
43 #[error("HTTP Request failed: {0}")]
45 RequestError(#[from] reqwest::Error),
46 #[error("Middleware error: {0}")]
48 MiddlewareError(#[from] reqwest_middleware::Error),
49 #[error("API error: {0}")]
51 ApiError(String),
52 #[error("User not found")]
54 UserNotFound,
55 #[error("Serialization error: {0}")]
57 SerializationError(#[from] serde_json::Error),
58 #[error("Token verification error: {0}")]
60 TokenVerificationError(#[from] TokenVerificationError),
61 #[error("JWT error: {0}")]
63 JwtError(#[from] jsonwebtoken::errors::Error),
64 #[error("Invalid private key")]
66 InvalidPrivateKey,
67 #[error("Service account key required for this operation")]
69 ServiceAccountKeyRequired,
70 #[error("Import users error: {0:?}")]
72 ImportUsersError(Vec<models::ImportUserError>),
73}
74
75#[derive(Debug, Serialize)]
77struct CustomTokenClaims {
78 iss: String,
79 sub: String,
80 aud: String,
81 iat: usize,
82 exp: usize,
83 uid: String,
84 #[serde(flatten)]
85 claims: Option<serde_json::Map<String, serde_json::Value>>,
86}
87
88#[derive(Clone)]
90pub struct FirebaseAuth {
91 client: ClientWithMiddleware,
92 base_url: String,
93 verifier: Arc<IdTokenVerifier>,
94 middleware: AuthMiddleware,
95 tenant_id: Option<String>,
96}
97
98impl FirebaseAuth {
99 pub fn new(middleware: AuthMiddleware) -> Self {
103 let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
104
105 let client = ClientBuilder::new(Client::new())
106 .with(RetryTransientMiddleware::new_with_policy(retry_policy))
107 .with(middleware.clone())
108 .build();
109
110 let key = &middleware.key;
111 let project_id = key.project_id.clone().unwrap_or_default();
112 let verifier = Arc::new(IdTokenVerifier::new(project_id.clone()));
113
114 let tenant_id = middleware.tenant_id();
115
116 let base_url = if let Some(tid) = &tenant_id {
117 AUTH_V1_TENANT_API
118 .replace("{project_id}", &project_id)
119 .replace("{tenant_id}", tid)
120 } else {
121 AUTH_V1_API.replace("{project_id}", &project_id)
122 };
123
124 Self {
125 client,
126 base_url,
127 verifier,
128 middleware,
129 tenant_id,
130 }
131 }
132
133 #[cfg(test)]
134 pub(crate) fn new_with_client(client: ClientWithMiddleware, base_url: String) -> Self {
135 let key = yup_oauth2::ServiceAccountKey {
138 key_type: Some("service_account".to_string()),
139 client_email: "test@example.com".to_string(),
140 private_key: "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC6\n-----END PRIVATE KEY-----".to_string(),
141 project_id: Some("test-project".to_string()),
142 private_key_id: None,
143 client_id: None,
144 auth_uri: None,
145 token_uri: "https://oauth2.googleapis.com/token".to_string(),
146 auth_provider_x509_cert_url: None,
147 client_x509_cert_url: None,
148 };
149 let middleware = AuthMiddleware::new(key);
150 let verifier = Arc::new(IdTokenVerifier::new("test-project".to_string()));
151
152 Self {
153 client,
154 base_url,
155 verifier,
156 middleware,
157 tenant_id: None,
158 }
159 }
160
161 pub fn tenant_manager(&self) -> TenantAwareness {
163 TenantAwareness::new(self.middleware.clone())
164 }
165
166 pub fn project_config_manager(&self) -> ProjectConfig {
168 ProjectConfig::new(self.middleware.clone())
169 }
170
171 pub async fn verify_id_token(&self, token: &str) -> Result<FirebaseTokenClaims, AuthError> {
180 Ok(self.verifier.verify_id_token(token).await?)
181 }
182
183 pub async fn create_session_cookie(
190 &self,
191 id_token: &str,
192 valid_duration: std::time::Duration,
193 ) -> Result<String, AuthError> {
194 let url = format!("{}:createSessionCookie", self.base_url);
195
196 let request = CreateSessionCookieRequest {
197 id_token: id_token.to_string(),
198 valid_duration_seconds: valid_duration.as_secs(),
199 };
200
201 let response = self
202 .client
203 .post(&url)
204 .header(header::CONTENT_TYPE, "application/json")
205 .body(serde_json::to_vec(&request)?)
206 .send()
207 .await?;
208
209 if !response.status().is_success() {
210 return Err(AuthError::ApiError(
211 parse_error_response(response, "Create session cookie failed").await,
212 ));
213 }
214
215 let result: CreateSessionCookieResponse = response.json().await?;
216 Ok(result.session_cookie)
217 }
218
219 pub async fn verify_session_cookie(
225 &self,
226 session_cookie: &str,
227 ) -> Result<FirebaseTokenClaims, AuthError> {
228 Ok(self.verifier.verify_session_cookie(session_cookie).await?)
229 }
230
231 pub fn create_custom_token(
240 &self,
241 uid: &str,
242 custom_claims: Option<serde_json::Map<String, serde_json::Value>>,
243 ) -> Result<String, AuthError> {
244 let key = &self.middleware.key;
245 let client_email = key.client_email.clone();
246 let private_key = key.private_key.clone();
247
248 let now = SystemTime::now()
249 .duration_since(UNIX_EPOCH)
250 .unwrap()
251 .as_secs() as usize;
252
253 let mut final_claims = custom_claims.unwrap_or_default();
254 if let Some(tid) = &self.tenant_id {
255 final_claims.insert(
256 "tenant_id".to_string(),
257 serde_json::Value::String(tid.clone()),
258 );
259 }
260
261 let claims = CustomTokenClaims {
262 iss: client_email.clone(),
263 sub: client_email,
264 aud: "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit".to_string(),
265 iat: now,
266 exp: now + 3600, uid: uid.to_string(),
268 claims: Some(final_claims),
269 };
270
271 let encoding_key = EncodingKey::from_rsa_pem(private_key.as_bytes())
272 .map_err(|_| AuthError::InvalidPrivateKey)?;
273
274 let header = Header::new(Algorithm::RS256);
275 let token = encode(&header, &claims, &encoding_key)?;
276
277 Ok(token)
278 }
279
280 async fn generate_email_link(
282 &self,
283 request_type: &str,
284 email: &str,
285 settings: Option<ActionCodeSettings>,
286 ) -> Result<String, AuthError> {
287 let url = format!("{}/accounts:sendOobCode", self.base_url,);
288
289 let mut request = EmailLinkRequest {
290 request_type: request_type.to_string(),
291 email: Some(email.to_string()),
292 ..Default::default()
293 };
294
295 if let Some(s) = settings {
296 request.continue_url = Some(s.url);
297 request.can_handle_code_in_app = s.handle_code_in_app;
298 request.dynamic_link_domain = s.dynamic_link_domain;
299
300 if let Some(ios) = s.ios {
301 request.ios_bundle_id = Some(ios.bundle_id);
302 }
303
304 if let Some(android) = s.android {
305 request.android_package_name = Some(android.package_name);
306 request.android_install_app = android.install_app;
307 request.android_minimum_version = android.minimum_version;
308 }
309 }
310
311 let response = self
312 .client
313 .post(&url)
314 .header(header::CONTENT_TYPE, "application/json")
315 .body(serde_json::to_vec(&request)?)
316 .send()
317 .await?;
318
319 if !response.status().is_success() {
320 return Err(AuthError::ApiError(
321 parse_error_response(response, "Generate email link failed").await,
322 ));
323 }
324
325 let result: EmailLinkResponse = response.json().await?;
326 Ok(result.oob_link)
327 }
328
329 pub async fn generate_password_reset_link(
331 &self,
332 email: &str,
333 settings: Option<ActionCodeSettings>,
334 ) -> Result<String, AuthError> {
335 self.generate_email_link("PASSWORD_RESET", email, settings)
336 .await
337 }
338
339 pub async fn generate_email_verification_link(
341 &self,
342 email: &str,
343 settings: Option<ActionCodeSettings>,
344 ) -> Result<String, AuthError> {
345 self.generate_email_link("VERIFY_EMAIL", email, settings)
346 .await
347 }
348
349 pub async fn generate_sign_in_with_email_link(
351 &self,
352 email: &str,
353 settings: Option<ActionCodeSettings>,
354 ) -> Result<String, AuthError> {
355 self.generate_email_link("EMAIL_SIGNIN", email, settings)
356 .await
357 }
358
359 pub async fn import_users(
365 &self,
366 request: ImportUsersRequest,
367 ) -> Result<ImportUsersResponse, AuthError> {
368 let url = format!("{}/accounts:batchCreate", self.base_url,);
369
370 let response = self
371 .client
372 .post(&url)
373 .header(header::CONTENT_TYPE, "application/json")
374 .body(serde_json::to_vec(&request)?)
375 .send()
376 .await?;
377
378 if !response.status().is_success() {
379 return Err(AuthError::ApiError(
380 parse_error_response(response, "Import users failed").await,
381 ));
382 }
383
384 let result: ImportUsersResponse = response.json().await?;
385
386 if let Some(errors) = &result.error {
387 if !errors.is_empty() {
388 return Err(AuthError::ImportUsersError(
394 errors
395 .iter()
396 .map(|e| models::ImportUserError {
397 index: e.index,
398 message: e.message.clone(),
399 })
400 .collect(),
401 ));
402 }
403 }
404
405 Ok(result)
406 }
407
408 pub async fn create_user(&self, request: CreateUserRequest) -> Result<UserRecord, AuthError> {
410 let url = format!("{}/accounts", self.base_url);
411
412 let response = self
413 .client
414 .post(&url)
415 .header(header::CONTENT_TYPE, "application/json")
416 .body(serde_json::to_vec(&request)?)
417 .send()
418 .await?;
419
420 if !response.status().is_success() {
421 return Err(AuthError::ApiError(
422 parse_error_response(response, "Create user failed").await,
423 ));
424 }
425
426 let user: UserRecord = response.json().await?;
427 Ok(user)
428 }
429
430 pub async fn update_user(&self, request: UpdateUserRequest) -> Result<UserRecord, AuthError> {
432 let url = format!("{}/accounts:update", self.base_url);
433
434 let response = self
435 .client
436 .post(&url)
437 .header(header::CONTENT_TYPE, "application/json")
438 .body(serde_json::to_vec(&request)?)
439 .send()
440 .await?;
441
442 if !response.status().is_success() {
443 return Err(AuthError::ApiError(
444 parse_error_response(response, "Update user failed").await,
445 ));
446 }
447
448 let user: UserRecord = response.json().await?;
449 Ok(user)
450 }
451
452 pub async fn delete_user(&self, uid: &str) -> Result<(), AuthError> {
454 let url = format!("{}/accounts:delete", self.base_url);
455 let request = DeleteAccountRequest {
456 local_id: uid.to_string(),
457 };
458
459 let response = self
460 .client
461 .post(&url)
462 .header(header::CONTENT_TYPE, "application/json")
463 .body(serde_json::to_vec(&request)?)
464 .send()
465 .await?;
466
467 if !response.status().is_success() {
468 return Err(AuthError::ApiError(
469 parse_error_response(response, "Delete user failed").await,
470 ));
471 }
472
473 Ok(())
474 }
475
476 async fn get_account_info(
478 &self,
479 request: GetAccountInfoRequest,
480 ) -> Result<UserRecord, AuthError> {
481 let url = format!("{}/accounts:lookup", self.base_url);
482
483 let response = self
484 .client
485 .post(&url)
486 .header(header::CONTENT_TYPE, "application/json")
487 .body(serde_json::to_vec(&request)?)
488 .send()
489 .await?;
490
491 if !response.status().is_success() {
492 return Err(AuthError::ApiError(
493 parse_error_response(response, "Get user failed").await,
494 ));
495 }
496
497 let result: GetAccountInfoResponse = response.json().await?;
498
499 result
500 .users
501 .and_then(|mut users| users.pop())
502 .ok_or(AuthError::UserNotFound)
503 }
504
505 pub async fn get_user(&self, uid: &str) -> Result<UserRecord, AuthError> {
507 let request = GetAccountInfoRequest {
508 local_id: Some(vec![uid.to_string()]),
509 email: None,
510 phone_number: None,
511 };
512 self.get_account_info(request).await
513 }
514
515 pub async fn get_user_by_email(&self, email: &str) -> Result<UserRecord, AuthError> {
517 let request = GetAccountInfoRequest {
518 local_id: None,
519 email: Some(vec![email.to_string()]),
520 phone_number: None,
521 };
522 self.get_account_info(request).await
523 }
524
525 pub async fn get_user_by_phone_number(&self, phone: &str) -> Result<UserRecord, AuthError> {
527 let request = GetAccountInfoRequest {
528 local_id: None,
529 email: None,
530 phone_number: Some(vec![phone.to_string()]),
531 };
532 self.get_account_info(request).await
533 }
534
535 pub async fn list_users(
542 &self,
543 max_results: u32,
544 page_token: Option<&str>,
545 ) -> Result<ListUsersResponse, AuthError> {
546 let url = format!("{}/accounts", self.base_url);
547 let mut url_obj = Url::parse(&url).map_err(|e| AuthError::ApiError(e.to_string()))?;
548
549 {
550 let mut query_pairs = url_obj.query_pairs_mut();
551 query_pairs.append_pair("maxResults", &max_results.to_string());
552 if let Some(token) = page_token {
553 query_pairs.append_pair("nextPageToken", token);
554 }
555 }
556
557 let response = self.client.get(url_obj).send().await?;
558
559 if !response.status().is_success() {
560 return Err(AuthError::ApiError(
561 parse_error_response(response, "List users failed").await,
562 ));
563 }
564
565 let result: ListUsersResponse = response.json().await?;
566 Ok(result)
567 }
568}
569
570#[cfg(test)]
571mod tests;