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 `Authorization: Bearer` token for calls to the Identity
100    /// Toolkit REST API.
101    ///
102    /// In emulator mode, returns the literal string `"owner"` — the magic
103    /// token the Firebase Auth Emulator recognizes as a privileged/admin
104    /// caller (confirmed against the emulator's own source, which gates
105    /// admin-only behavior in `accounts:signUp` and similar operations on
106    /// whether the request carries recognized OAuth2 credentials; the
107    /// official Admin SDKs send this same literal in emulator mode). Without
108    /// it, admin calls like creating a user are instead treated as
109    /// unprivileged client requests and rejected. In live mode, resolves a
110    /// real OAuth2 access token from the configured credentials, requiring
111    /// the `live-user-management` feature; without it, user-management calls
112    /// in live mode fail with a clear error rather than silently sending an
113    /// unauthenticated request.
114    async fn bearer_token(&self) -> Result<Option<String>, AuthError> {
115        if !self.mode.requires_bearer_token() {
116            return Ok(self.mode.emulator_bearer_token().map(str::to_string));
117        }
118
119        #[cfg(feature = "live-user-management")]
120        {
121            let provider = self
122                .token_provider
123                .get_or_try_init(|| async {
124                    match &self.credentials {
125                        Credentials::ServiceAccount(key) => {
126                            crate::auth::token_provider::TokenProvider::from_service_account(key)
127                        }
128                        Credentials::ApplicationDefault => {
129                            crate::auth::token_provider::TokenProvider::from_application_default()
130                                .await
131                        }
132                        Credentials::Emulator => unreachable!(
133                            "requires_bearer_token() is false in emulator mode; \
134                             bearer_token() returns before reaching this branch"
135                        ),
136                    }
137                })
138                .await?;
139            return provider.access_token().await.map(Some);
140        }
141
142        #[cfg(not(feature = "live-user-management"))]
143        {
144            Err(AuthError::Core(crate::core::CoreError::Credentials(
145                "user management against production Firebase requires the \
146                 `live-user-management` feature"
147                    .to_string(),
148            )))
149        }
150    }
151
152    fn user_operations<'a>(&'a self, bearer_token: Option<&'a str>) -> UserOperations<'a> {
153        UserOperations::new(
154            &self.http,
155            &self.endpoints,
156            bearer_token,
157            self.mode.emulator_api_key(),
158        )
159    }
160
161    /// Fetches a user by uid.
162    pub async fn get_user(&self, uid: &str) -> Result<UserRecord, AuthError> {
163        let token = self.bearer_token().await?;
164        self.user_operations(token.as_deref()).get_user(uid).await
165    }
166
167    /// Fetches a user by email address.
168    pub async fn get_user_by_email(&self, email: &str) -> Result<UserRecord, AuthError> {
169        let token = self.bearer_token().await?;
170        self.user_operations(token.as_deref())
171            .get_user_by_email(email)
172            .await
173    }
174
175    /// Creates a new user.
176    pub async fn create_user(&self, request: CreateUserRequest) -> Result<UserRecord, AuthError> {
177        let token = self.bearer_token().await?;
178        self.user_operations(token.as_deref())
179            .create_user(request)
180            .await
181    }
182
183    /// Updates an existing user.
184    pub async fn update_user(
185        &self,
186        uid: &str,
187        request: UpdateUserRequest,
188    ) -> Result<UserRecord, AuthError> {
189        let token = self.bearer_token().await?;
190        self.user_operations(token.as_deref())
191            .update_user(uid, request)
192            .await
193    }
194
195    /// Replaces a user's custom claims.
196    pub async fn set_custom_user_claims(
197        &self,
198        uid: &str,
199        claims: serde_json::Map<String, serde_json::Value>,
200    ) -> Result<(), AuthError> {
201        let token = self.bearer_token().await?;
202        self.user_operations(token.as_deref())
203            .set_custom_user_claims(uid, claims)
204            .await
205    }
206
207    /// Deletes a user by uid.
208    pub async fn delete_user(&self, uid: &str) -> Result<(), AuthError> {
209        let token = self.bearer_token().await?;
210        self.user_operations(token.as_deref())
211            .delete_user(uid)
212            .await
213    }
214
215    /// Lists users, paginated via `next_page_token`.
216    pub async fn list_users(
217        &self,
218        max_results: u32,
219        page_token: Option<&str>,
220    ) -> Result<UserPage, AuthError> {
221        let token = self.bearer_token().await?;
222        self.user_operations(token.as_deref())
223            .list_users(max_results, page_token)
224            .await
225    }
226}
227
228/// Builds an [`AuthClient`].
229pub struct AuthClientBuilder {
230    project_id: String,
231    service_account: Option<ServiceAccountKey>,
232    #[cfg(feature = "live-user-management")]
233    use_application_default_credentials: bool,
234    emulator_host: Option<String>,
235    http_client: Option<reqwest::Client>,
236}
237
238impl AuthClientBuilder {
239    /// Starts building a client for the given Firebase project id.
240    pub fn new(project_id: impl Into<String>) -> Self {
241        Self {
242            project_id: project_id.into(),
243            service_account: None,
244            #[cfg(feature = "live-user-management")]
245            use_application_default_credentials: false,
246            emulator_host: None,
247            http_client: None,
248        }
249    }
250
251    /// Authenticates using an explicit service account key.
252    pub fn service_account_key(mut self, key: ServiceAccountKey) -> Self {
253        self.service_account = Some(key);
254        self
255    }
256
257    /// Authenticates using Application Default Credentials, resolved on
258    /// first use: the `GOOGLE_APPLICATION_CREDENTIALS` environment
259    /// variable, gcloud user credentials, or the GCE/Cloud Run metadata
260    /// server, in that order (see [`gcp_auth::provider`]).
261    #[cfg(feature = "live-user-management")]
262    pub fn application_default_credentials(mut self) -> Self {
263        self.use_application_default_credentials = true;
264        self
265    }
266
267    /// Targets a Firebase Auth Emulator at `host` (e.g. `localhost:9099`)
268    /// instead of production Firebase.
269    ///
270    /// If not called, the client still auto-detects the
271    /// `FIREBASE_AUTH_EMULATOR_HOST` environment variable in [`Self::build`].
272    #[cfg(feature = "emulator")]
273    pub fn use_emulator(mut self, host: impl Into<String>) -> Self {
274        self.emulator_host = Some(host.into());
275        self
276    }
277
278    /// Supplies a custom [`reqwest::Client`], e.g. for testing.
279    pub fn http_client(mut self, client: reqwest::Client) -> Self {
280        self.http_client = Some(client);
281        self
282    }
283
284    /// Builds the [`AuthClient`].
285    pub fn build(self) -> Result<AuthClient, AuthError> {
286        let project_id = ProjectId::new(self.project_id)?;
287        let mode = ClientMode::resolve(self.emulator_host);
288
289        let credentials = if let Some(key) = self.service_account {
290            Credentials::ServiceAccount(Box::new(key))
291        } else {
292            #[cfg(feature = "live-user-management")]
293            if self.use_application_default_credentials {
294                Credentials::ApplicationDefault
295            } else if matches!(mode, ClientMode::Emulator { .. }) {
296                Credentials::Emulator
297            } else {
298                return Err(AuthError::Core(crate::core::CoreError::Credentials(
299                    "no credentials configured: call service_account_key(...) or \
300                     application_default_credentials()"
301                        .to_string(),
302                )));
303            }
304            #[cfg(not(feature = "live-user-management"))]
305            if matches!(mode, ClientMode::Emulator { .. }) {
306                Credentials::Emulator
307            } else {
308                return Err(AuthError::Core(crate::core::CoreError::Credentials(
309                    "no credentials configured: call service_account_key(...)".to_string(),
310                )));
311            }
312        };
313
314        let http = HttpClient::new(self.http_client.unwrap_or_default());
315        let endpoints = mode.endpoints(project_id.as_str());
316        let jwks = JwksCache::new(http.clone());
317        let id_token_verifier = IdTokenVerifier::new(project_id.clone(), jwks);
318        let session_cookie_certs = SessionCookieCertCache::new(http.clone());
319        let session_cookie_verifier =
320            SessionCookieVerifier::new(project_id.clone(), session_cookie_certs);
321
322        Ok(AuthClient {
323            http,
324            project_id,
325            mode,
326            credentials,
327            id_token_verifier,
328            session_cookie_verifier,
329            endpoints,
330            #[cfg(feature = "live-user-management")]
331            token_provider: tokio::sync::OnceCell::new(),
332        })
333    }
334}