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 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/// Errors that can occur during Authentication operations.
40#[derive(Error, Debug)]
41pub enum AuthError {
42    /// Wrapper for `reqwest::Error`.
43    #[error("HTTP Request failed: {0}")]
44    RequestError(#[from] reqwest::Error),
45    /// Wrapper for `reqwest_middleware::Error`.
46    #[error("Middleware error: {0}")]
47    MiddlewareError(#[from] reqwest_middleware::Error),
48    /// Errors returned by the Identity Toolkit API.
49    #[error("API error: {0}")]
50    ApiError(String),
51    /// The requested user was not found.
52    #[error("User not found")]
53    UserNotFound,
54    /// Wrapper for `serde_json::Error`.
55    #[error("Serialization error: {0}")]
56    SerializationError(#[from] serde_json::Error),
57    /// Error during ID token verification.
58    #[error("Token verification error: {0}")]
59    TokenVerificationError(#[from] TokenVerificationError),
60    /// Wrapper for `jsonwebtoken::errors::Error`.
61    #[error("JWT error: {0}")]
62    JwtError(#[from] jsonwebtoken::errors::Error),
63    /// The private key provided in the service account is invalid.
64    #[error("Invalid private key")]
65    InvalidPrivateKey,
66    /// A service account key is required for this operation (e.g., custom token signing) but was not provided.
67    #[error("Service account key required for this operation")]
68    ServiceAccountKeyRequired,
69    /// Errors occurred during a bulk import operation.
70    #[error("Import users error: {0:?}")]
71    ImportUsersError(Vec<models::ImportUserError>),
72}
73
74/// Claims used for generating custom tokens.
75#[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/// Client for interacting with Firebase Authentication.
88#[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    /// Creates a new `FirebaseAuth` instance.
99    ///
100    /// This is typically called via `FirebaseApp::auth()`.
101    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        // We need a dummy middleware and verifier for the struct, but we won't use them for this test
133        // Ideally we'd have a builder or optionals, but for now we construct dummies.
134        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    /// Returns the tenant awareness interface.
159    pub fn tenant_manager(&self) -> TenantAwareness {
160        TenantAwareness::new(self.middleware.clone())
161    }
162
163    /// Returns the project config interface.
164    pub fn project_config_manager(&self) -> ProjectConfig {
165        ProjectConfig::new(self.middleware.clone())
166    }
167
168    /// Verifies a Firebase ID token.
169    ///
170    /// This method fetches Google's public keys (caching them respecting Cache-Control)
171    /// and verifies the signature, audience, issuer, and expiration of the token.
172    ///
173    /// # Arguments
174    ///
175    /// * `token` - The JWT ID token string.
176    pub async fn verify_id_token(&self, token: &str) -> Result<FirebaseTokenClaims, AuthError> {
177        Ok(self.verifier.verify_id_token(token).await?)
178    }
179
180    /// Creates a session cookie from an ID token.
181    ///
182    /// # Arguments
183    ///
184    /// * `id_token` - The ID token to exchange for a session cookie.
185    /// * `valid_duration` - The duration for which the session cookie is valid.
186    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    /// Verifies a Firebase session cookie.
215    ///
216    /// # Arguments
217    ///
218    /// * `session_cookie` - The session cookie string.
219    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    /// Creates a custom token for the given UID with optional custom claims.
227    ///
228    /// This token can be sent to a client application to sign in with `signInWithCustomToken`.
229    ///
230    /// # Arguments
231    ///
232    /// * `uid` - The unique identifier for the user.
233    /// * `custom_claims` - Optional JSON object containing custom claims.
234    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, // 1 hour expiration
259            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    /// Internal helper to generate OOB (Out-of-Band) email links.
273    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    /// Generates a link for password reset.
320    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    /// Generates a link for email verification.
330    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    /// Generates a link for sign-in with email.
340    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    /// Imports users in bulk.
350    ///
351    /// # Arguments
352    ///
353    /// * `request` - An `ImportUsersRequest` containing the list of users and hashing algorithm configuration.
354    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                // Partial failure or full failure reporting depending on API behavior
377                // Usually batchCreate returns 200 with errors list for partials.
378                // We can return the response or error out.
379                // Let's return the response but user should check it.
380                // Or we can define that if errors exist, we return Err(AuthError::ImportUsersError(errors))
381                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    /// Creates a new user.
397    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    /// Updates an existing user.
417    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    /// Deletes a user by UID.
437    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    /// Internal helper to get account info.
459    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    /// Retrieves a user by their UID.
486    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    /// Retrieves a user by their email.
496    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    /// Retrieves a user by their phone number.
506    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    /// Lists users.
516    ///
517    /// # Arguments
518    ///
519    /// * `max_results` - The maximum number of users to return.
520    /// * `page_token` - The next page token from a previous response.
521    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;