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