Skip to main content

firebase_admin_sdk/auth/
mod.rs

1//! Firebase Authentication module.
2//!
3//! This module provides functionality for managing users (create, update, delete, list, get)
4//! and generating OOB (Out-of-Band) codes for email actions like password resets and email verification.
5//! It also includes ID token verification.
6
7pub 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/// Errors that can occur during Authentication operations.
41#[derive(Error, Debug)]
42pub enum AuthError {
43    /// Wrapper for `reqwest::Error`.
44    #[error("HTTP Request failed: {0}")]
45    RequestError(#[from] reqwest::Error),
46    /// Wrapper for `reqwest_middleware::Error`.
47    #[error("Middleware error: {0}")]
48    MiddlewareError(#[from] reqwest_middleware::Error),
49    /// Errors returned by the Identity Toolkit API.
50    #[error("API error: {0}")]
51    ApiError(String),
52    /// The requested user was not found.
53    #[error("User not found")]
54    UserNotFound,
55    /// Wrapper for `serde_json::Error`.
56    #[error("Serialization error: {0}")]
57    SerializationError(#[from] serde_json::Error),
58    /// Error during ID token verification.
59    #[error("Token verification error: {0}")]
60    TokenVerificationError(#[from] TokenVerificationError),
61    /// Wrapper for `jsonwebtoken::errors::Error`.
62    #[error("JWT error: {0}")]
63    JwtError(#[from] jsonwebtoken::errors::Error),
64    /// The private key provided in the service account is invalid.
65    #[error("Invalid private key")]
66    InvalidPrivateKey,
67    /// A service account key is required for this operation (e.g., custom token signing) but was not provided.
68    #[error("Service account key required for this operation")]
69    ServiceAccountKeyRequired,
70    /// Errors occurred during a bulk import operation.
71    #[error("Import users error: {0:?}")]
72    ImportUsersError(Vec<models::ImportUserError>),
73}
74
75/// Claims used for generating custom tokens.
76#[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/// Client for interacting with Firebase Authentication.
89#[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    /// Creates a new `FirebaseAuth` instance.
100    ///
101    /// This is typically called via `FirebaseApp::auth()`.
102    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        // We need a dummy middleware and verifier for the struct, but we won't use them for this test
136        // Ideally we'd have a builder or optionals, but for now we construct dummies.
137        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    /// Returns the tenant awareness interface.
162    pub fn tenant_manager(&self) -> TenantAwareness {
163        TenantAwareness::new(self.middleware.clone())
164    }
165
166    /// Returns the project config interface.
167    pub fn project_config_manager(&self) -> ProjectConfig {
168        ProjectConfig::new(self.middleware.clone())
169    }
170
171    /// Verifies a Firebase ID token.
172    ///
173    /// This method fetches Google's public keys (caching them respecting Cache-Control)
174    /// and verifies the signature, audience, issuer, and expiration of the token.
175    ///
176    /// # Arguments
177    ///
178    /// * `token` - The JWT ID token string.
179    pub async fn verify_id_token(&self, token: &str) -> Result<FirebaseTokenClaims, AuthError> {
180        Ok(self.verifier.verify_id_token(token).await?)
181    }
182
183    /// Creates a session cookie from an ID token.
184    ///
185    /// # Arguments
186    ///
187    /// * `id_token` - The ID token to exchange for a session cookie.
188    /// * `valid_duration` - The duration for which the session cookie is valid.
189    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    /// Verifies a Firebase session cookie.
220    ///
221    /// # Arguments
222    ///
223    /// * `session_cookie` - The session cookie string.
224    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    /// Creates a custom token for the given UID with optional custom claims.
232    ///
233    /// This token can be sent to a client application to sign in with `signInWithCustomToken`.
234    ///
235    /// # Arguments
236    ///
237    /// * `uid` - The unique identifier for the user.
238    /// * `custom_claims` - Optional JSON object containing custom claims.
239    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, // 1 hour expiration
267            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    /// Internal helper to generate OOB (Out-of-Band) email links.
281    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    /// Generates a link for password reset.
330    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    /// Generates a link for email verification.
340    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    /// Generates a link for sign-in with email.
350    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    /// Imports users in bulk.
360    ///
361    /// # Arguments
362    ///
363    /// * `request` - An `ImportUsersRequest` containing the list of users and hashing algorithm configuration.
364    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                // Partial failure or full failure reporting depending on API behavior
389                // Usually batchCreate returns 200 with errors list for partials.
390                // We can return the response or error out.
391                // Let's return the response but user should check it.
392                // Or we can define that if errors exist, we return Err(AuthError::ImportUsersError(errors))
393                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    /// Creates a new user.
409    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    /// Updates an existing user.
431    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    /// Deletes a user by UID.
453    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    /// Internal helper to get account info.
477    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    /// Retrieves a user by their UID.
506    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    /// Retrieves a user by their email.
516    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    /// Retrieves a user by their phone number.
526    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    /// Lists users.
536    ///
537    /// # Arguments
538    ///
539    /// * `max_results` - The maximum number of users to return.
540    /// * `page_token` - The next page token from a previous response.
541    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;