Skip to main content

firebase_admin/auth/
client.rs

1//! The `AuthClient` entry point and its builder.
2
3use crate::auth::custom_token::CustomTokenSigner;
4use crate::auth::error::AuthError;
5use crate::auth::id_token::{IdTokenClaims, IdTokenVerifier, JwksCache};
6use crate::auth::identity_toolkit::IdentityToolkitEndpoints;
7use crate::auth::mode::ClientMode;
8use crate::auth::session_cookie::{SessionCookieCertCache, SessionCookieVerifier};
9use crate::auth::users::{
10    CreateUserRequest, UpdateUserRequest, UserOperations, UserPage, UserRecord,
11};
12use crate::core::{Credentials, HttpClient, ProjectId, ServiceAccountKey};
13use std::time::Duration;
14
15/// Firebase Authentication client.
16///
17/// A single concrete type serves both live and emulator use: mode is a
18/// runtime field ([`ClientMode`]), not a type parameter, so every method
19/// below has exactly one signature regardless of environment. Build one with
20/// [`AuthClientBuilder`].
21pub struct AuthClient {
22    http: HttpClient,
23    project_id: ProjectId,
24    mode: ClientMode,
25    credentials: Credentials,
26    id_token_verifier: IdTokenVerifier,
27    session_cookie_verifier: SessionCookieVerifier,
28    endpoints: IdentityToolkitEndpoints,
29    #[cfg(feature = "live-user-management")]
30    token_provider: tokio::sync::OnceCell<crate::auth::token_provider::TokenProvider>,
31}
32
33impl AuthClient {
34    /// Starts building a new client for the given Firebase project.
35    pub fn builder(project_id: impl Into<String>) -> AuthClientBuilder {
36        AuthClientBuilder::new(project_id)
37    }
38
39    /// Returns the Firebase project id this client is configured for.
40    pub fn project_id(&self) -> &ProjectId {
41        &self.project_id
42    }
43
44    /// Verifies a Firebase ID token, returning its claims.
45    pub async fn verify_id_token(
46        &self,
47        token: &str,
48    ) -> Result<crate::auth::id_token::IdTokenClaims, AuthError> {
49        Ok(self.id_token_verifier.verify(token).await?)
50    }
51
52    /// Creates a Firebase custom token for the given uid.
53    ///
54    /// Requires the client to have been built with an explicit service
55    /// account key; Application Default Credentials do not expose a
56    /// private key and cannot sign custom tokens.
57    pub fn create_custom_token(
58        &self,
59        uid: &str,
60        claims: Option<serde_json::Map<String, serde_json::Value>>,
61    ) -> Result<String, AuthError> {
62        let Credentials::ServiceAccount(key) = &self.credentials else {
63            return Err(AuthError::Core(crate::core::CoreError::Credentials(
64                "create_custom_token requires an explicit service account key".to_string(),
65            )));
66        };
67        let signer = CustomTokenSigner::new((**key).clone());
68        Ok(signer.create_custom_token(uid, claims)?)
69    }
70
71    /// Exchanges a verified ID token for a session cookie valid for
72    /// `valid_duration` (Firebase allows up to 14 days).
73    pub async fn create_session_cookie(
74        &self,
75        id_token: &str,
76        valid_duration: Duration,
77    ) -> Result<String, AuthError> {
78        let token = self.bearer_token().await?;
79        crate::auth::session_cookie::create_session_cookie(
80            &self.http,
81            &self.endpoints.create_session_cookie(),
82            id_token,
83            valid_duration,
84            token.as_deref(),
85            self.mode.emulator_api_key(),
86        )
87        .await
88    }
89
90    /// Verifies a Firebase session cookie, returning its claims.
91    ///
92    /// Verified against a different certificate endpoint and issuer than
93    /// [`Self::verify_id_token`] — see [`crate::auth::session_cookie::verify`]
94    /// for why the two aren't interchangeable.
95    pub async fn verify_session_cookie(&self, cookie: &str) -> Result<IdTokenClaims, AuthError> {
96        Ok(self.session_cookie_verifier.verify(cookie).await?)
97    }
98
99    /// Resolves an OAuth2 bearer token for calls to the Identity Toolkit
100    /// REST API.
101    ///
102    /// Returns `None` when talking to the emulator (which doesn't require
103    /// authentication) or in any configuration where a token isn't needed.
104    /// Requires the `live-user-management` feature when talking to
105    /// production Firebase; without it, user-management calls in live mode
106    /// fail with a clear error rather than silently sending an
107    /// unauthenticated request.
108    async fn bearer_token(&self) -> Result<Option<String>, AuthError> {
109        if !self.mode.requires_bearer_token() {
110            return Ok(None);
111        }
112
113        #[cfg(feature = "live-user-management")]
114        {
115            let provider = self
116                .token_provider
117                .get_or_try_init(|| async {
118                    match &self.credentials {
119                        Credentials::ServiceAccount(key) => {
120                            crate::auth::token_provider::TokenProvider::from_service_account(key)
121                        }
122                        Credentials::ApplicationDefault => {
123                            crate::auth::token_provider::TokenProvider::from_application_default()
124                                .await
125                        }
126                        Credentials::Emulator => unreachable!(
127                            "requires_bearer_token() is false in emulator mode; \
128                             bearer_token() returns before reaching this branch"
129                        ),
130                    }
131                })
132                .await?;
133            return provider.access_token().await.map(Some);
134        }
135
136        #[cfg(not(feature = "live-user-management"))]
137        {
138            Err(AuthError::Core(crate::core::CoreError::Credentials(
139                "user management against production Firebase requires the \
140                 `live-user-management` feature"
141                    .to_string(),
142            )))
143        }
144    }
145
146    fn user_operations<'a>(&'a self, bearer_token: Option<&'a str>) -> UserOperations<'a> {
147        UserOperations::new(
148            &self.http,
149            &self.endpoints,
150            bearer_token,
151            self.mode.emulator_api_key(),
152        )
153    }
154
155    /// Fetches a user by uid.
156    pub async fn get_user(&self, uid: &str) -> Result<UserRecord, AuthError> {
157        let token = self.bearer_token().await?;
158        self.user_operations(token.as_deref()).get_user(uid).await
159    }
160
161    /// Fetches a user by email address.
162    pub async fn get_user_by_email(&self, email: &str) -> Result<UserRecord, AuthError> {
163        let token = self.bearer_token().await?;
164        self.user_operations(token.as_deref())
165            .get_user_by_email(email)
166            .await
167    }
168
169    /// Creates a new user.
170    pub async fn create_user(&self, request: CreateUserRequest) -> Result<UserRecord, AuthError> {
171        let token = self.bearer_token().await?;
172        self.user_operations(token.as_deref())
173            .create_user(request)
174            .await
175    }
176
177    /// Updates an existing user.
178    pub async fn update_user(
179        &self,
180        uid: &str,
181        request: UpdateUserRequest,
182    ) -> Result<UserRecord, AuthError> {
183        let token = self.bearer_token().await?;
184        self.user_operations(token.as_deref())
185            .update_user(uid, request)
186            .await
187    }
188
189    /// Replaces a user's custom claims.
190    pub async fn set_custom_user_claims(
191        &self,
192        uid: &str,
193        claims: serde_json::Map<String, serde_json::Value>,
194    ) -> Result<(), AuthError> {
195        let token = self.bearer_token().await?;
196        self.user_operations(token.as_deref())
197            .set_custom_user_claims(uid, claims)
198            .await
199    }
200
201    /// Deletes a user by uid.
202    pub async fn delete_user(&self, uid: &str) -> Result<(), AuthError> {
203        let token = self.bearer_token().await?;
204        self.user_operations(token.as_deref())
205            .delete_user(uid)
206            .await
207    }
208
209    /// Lists users, paginated via `next_page_token`.
210    pub async fn list_users(
211        &self,
212        max_results: u32,
213        page_token: Option<&str>,
214    ) -> Result<UserPage, AuthError> {
215        let token = self.bearer_token().await?;
216        self.user_operations(token.as_deref())
217            .list_users(max_results, page_token)
218            .await
219    }
220}
221
222/// Builds an [`AuthClient`].
223pub struct AuthClientBuilder {
224    project_id: String,
225    service_account: Option<ServiceAccountKey>,
226    #[cfg(feature = "live-user-management")]
227    use_application_default_credentials: bool,
228    emulator_host: Option<String>,
229    http_client: Option<reqwest::Client>,
230}
231
232impl AuthClientBuilder {
233    /// Starts building a client for the given Firebase project id.
234    pub fn new(project_id: impl Into<String>) -> Self {
235        Self {
236            project_id: project_id.into(),
237            service_account: None,
238            #[cfg(feature = "live-user-management")]
239            use_application_default_credentials: false,
240            emulator_host: None,
241            http_client: None,
242        }
243    }
244
245    /// Authenticates using an explicit service account key.
246    pub fn service_account_key(mut self, key: ServiceAccountKey) -> Self {
247        self.service_account = Some(key);
248        self
249    }
250
251    /// Authenticates using Application Default Credentials, resolved on
252    /// first use: the `GOOGLE_APPLICATION_CREDENTIALS` environment
253    /// variable, gcloud user credentials, or the GCE/Cloud Run metadata
254    /// server, in that order (see [`gcp_auth::provider`]).
255    #[cfg(feature = "live-user-management")]
256    pub fn application_default_credentials(mut self) -> Self {
257        self.use_application_default_credentials = true;
258        self
259    }
260
261    /// Targets a Firebase Auth Emulator at `host` (e.g. `localhost:9099`)
262    /// instead of production Firebase.
263    ///
264    /// If not called, the client still auto-detects the
265    /// `FIREBASE_AUTH_EMULATOR_HOST` environment variable in [`Self::build`].
266    #[cfg(feature = "emulator")]
267    pub fn use_emulator(mut self, host: impl Into<String>) -> Self {
268        self.emulator_host = Some(host.into());
269        self
270    }
271
272    /// Supplies a custom [`reqwest::Client`], e.g. for testing.
273    pub fn http_client(mut self, client: reqwest::Client) -> Self {
274        self.http_client = Some(client);
275        self
276    }
277
278    /// Builds the [`AuthClient`].
279    pub fn build(self) -> Result<AuthClient, AuthError> {
280        let project_id = ProjectId::new(self.project_id)?;
281        let mode = ClientMode::resolve(self.emulator_host);
282
283        let credentials = if let Some(key) = self.service_account {
284            Credentials::ServiceAccount(Box::new(key))
285        } else {
286            #[cfg(feature = "live-user-management")]
287            if self.use_application_default_credentials {
288                Credentials::ApplicationDefault
289            } else if matches!(mode, ClientMode::Emulator { .. }) {
290                Credentials::Emulator
291            } else {
292                return Err(AuthError::Core(crate::core::CoreError::Credentials(
293                    "no credentials configured: call service_account_key(...) or \
294                     application_default_credentials()"
295                        .to_string(),
296                )));
297            }
298            #[cfg(not(feature = "live-user-management"))]
299            if matches!(mode, ClientMode::Emulator { .. }) {
300                Credentials::Emulator
301            } else {
302                return Err(AuthError::Core(crate::core::CoreError::Credentials(
303                    "no credentials configured: call service_account_key(...)".to_string(),
304                )));
305            }
306        };
307
308        let http = HttpClient::new(self.http_client.unwrap_or_default());
309        let endpoints = mode.endpoints();
310        let jwks = JwksCache::new(http.clone());
311        let id_token_verifier = IdTokenVerifier::new(project_id.clone(), jwks);
312        let session_cookie_certs = SessionCookieCertCache::new(http.clone());
313        let session_cookie_verifier =
314            SessionCookieVerifier::new(project_id.clone(), session_cookie_certs);
315
316        Ok(AuthClient {
317            http,
318            project_id,
319            mode,
320            credentials,
321            id_token_verifier,
322            session_cookie_verifier,
323            endpoints,
324            #[cfg(feature = "live-user-management")]
325            token_provider: tokio::sync::OnceCell::new(),
326        })
327    }
328}