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