Skip to main content

firebase_admin/auth/users/
operations.rs

1//! User management operations against the Identity Toolkit REST API.
2
3use crate::auth::error::{parse_identity_toolkit_response, AuthError};
4use crate::auth::identity_toolkit::requests::{
5    AccountsResponse, DeleteRequest, LookupRequest, SignUpRequest, UpdateRequest,
6};
7use crate::auth::identity_toolkit::IdentityToolkitEndpoints;
8use crate::auth::users::model::{CreateUserRequest, UpdateUserRequest, UserRecord};
9use crate::auth::users::query::UserPage;
10use crate::core::{CoreError, HttpClient};
11
12/// Performs user-management calls against the Identity Toolkit REST API.
13///
14/// Requires an OAuth2 bearer token (obtained from the configured service
15/// account or Application Default Credentials) on every request except when
16/// targeting the Firebase Auth Emulator, which instead requires a `key=`
17/// query parameter to be present (any value; the emulator does not validate
18/// it) — see [`crate::auth::mode::ClientMode::emulator_api_key`].
19pub struct UserOperations<'a> {
20    http: &'a HttpClient,
21    endpoints: &'a IdentityToolkitEndpoints,
22    bearer_token: Option<&'a str>,
23    emulator_api_key: Option<&'static str>,
24}
25
26impl<'a> UserOperations<'a> {
27    /// Creates a new set of user operations bound to the given transport
28    /// and (when talking to production) bearer token, or (when talking to
29    /// the emulator) dummy API key.
30    pub fn new(
31        http: &'a HttpClient,
32        endpoints: &'a IdentityToolkitEndpoints,
33        bearer_token: Option<&'a str>,
34        emulator_api_key: Option<&'static str>,
35    ) -> Self {
36        Self {
37            http,
38            endpoints,
39            bearer_token,
40            emulator_api_key,
41        }
42    }
43
44    fn request(&self, url: &str) -> reqwest::RequestBuilder {
45        let mut builder = self.http.inner().post(url);
46        if let Some(token) = self.bearer_token {
47            builder = builder.bearer_auth(token);
48        }
49        if let Some(key) = self.emulator_api_key {
50            builder = builder.query(&[("key", key)]);
51        }
52        builder
53    }
54
55    /// Fetches a single user by uid.
56    pub async fn get_user(&self, uid: &str) -> Result<UserRecord, AuthError> {
57        let request = LookupRequest {
58            local_id: vec![uid.to_string()],
59            email: vec![],
60        };
61        let response = self
62            .request(&self.endpoints.lookup())
63            .json(&request)
64            .send()
65            .await?;
66        let parsed: AccountsResponse = parse_identity_toolkit_response(response).await?;
67        parsed
68            .users
69            .into_iter()
70            .next()
71            .map(UserRecord::from)
72            .ok_or(AuthError::UserNotFound)
73    }
74
75    /// Fetches a single user by email address.
76    pub async fn get_user_by_email(&self, email: &str) -> Result<UserRecord, AuthError> {
77        let request = LookupRequest {
78            local_id: vec![],
79            email: vec![email.to_string()],
80        };
81        let response = self
82            .request(&self.endpoints.lookup())
83            .json(&request)
84            .send()
85            .await?;
86        let parsed: AccountsResponse = parse_identity_toolkit_response(response).await?;
87        parsed
88            .users
89            .into_iter()
90            .next()
91            .map(UserRecord::from)
92            .ok_or(AuthError::UserNotFound)
93    }
94
95    /// Creates a new user.
96    pub async fn create_user(&self, request: CreateUserRequest) -> Result<UserRecord, AuthError> {
97        let body = SignUpRequest {
98            local_id: request.uid,
99            email: request.email,
100            password: request.password,
101            display_name: request.display_name,
102            disabled: request.disabled,
103        };
104        let response = self
105            .request(&self.endpoints.sign_up())
106            .json(&body)
107            .send()
108            .await?;
109        let local_id: serde_json::Value = parse_identity_toolkit_response(response).await?;
110        let uid = local_id
111            .get("localId")
112            .and_then(|v| v.as_str())
113            .ok_or_else(|| AuthError::Api {
114                status: 200,
115                message: "signUp response missing localId".to_string(),
116                error_code: None,
117            })?;
118        self.get_user(uid).await
119    }
120
121    /// Updates an existing user, including replacing its custom claims.
122    pub async fn update_user(
123        &self,
124        uid: &str,
125        request: UpdateUserRequest,
126    ) -> Result<UserRecord, AuthError> {
127        let custom_attributes = request
128            .custom_claims
129            .map(|claims| serde_json::to_string(&claims))
130            .transpose()
131            .map_err(CoreError::Deserialize)?;
132
133        let body = UpdateRequest {
134            local_id: uid.to_string(),
135            email: request.email,
136            display_name: request.display_name,
137            disable_user: request.disabled,
138            custom_attributes,
139        };
140        let response = self
141            .request(&self.endpoints.update())
142            .json(&body)
143            .send()
144            .await?;
145        let _: serde_json::Value = parse_identity_toolkit_response(response).await?;
146        self.get_user(uid).await
147    }
148
149    /// Replaces a user's custom claims entirely.
150    pub async fn set_custom_user_claims(
151        &self,
152        uid: &str,
153        claims: serde_json::Map<String, serde_json::Value>,
154    ) -> Result<(), AuthError> {
155        self.update_user(
156            uid,
157            UpdateUserRequest {
158                custom_claims: Some(claims),
159                ..Default::default()
160            },
161        )
162        .await?;
163        Ok(())
164    }
165
166    /// Deletes a user by uid.
167    pub async fn delete_user(&self, uid: &str) -> Result<(), AuthError> {
168        let body = DeleteRequest {
169            local_id: uid.to_string(),
170        };
171        let response = self
172            .request(&self.endpoints.delete())
173            .json(&body)
174            .send()
175            .await?;
176        let _: serde_json::Value = parse_identity_toolkit_response(response).await?;
177        Ok(())
178    }
179
180    /// Lists users, paginated via `next_page_token`.
181    pub async fn list_users(
182        &self,
183        max_results: u32,
184        page_token: Option<&str>,
185    ) -> Result<UserPage, AuthError> {
186        #[derive(serde::Serialize)]
187        struct BatchGetQuery<'a> {
188            #[serde(rename = "maxResults")]
189            max_results: u32,
190            #[serde(rename = "nextPageToken", skip_serializing_if = "Option::is_none")]
191            next_page_token: Option<&'a str>,
192        }
193
194        let response = self
195            .request(&self.endpoints.batch_get())
196            .json(&BatchGetQuery {
197                max_results,
198                next_page_token: page_token,
199            })
200            .send()
201            .await?;
202        let parsed: AccountsResponse = parse_identity_toolkit_response(response).await?;
203
204        Ok(UserPage {
205            users: parsed.users.into_iter().map(UserRecord::from).collect(),
206            next_page_token: parsed.next_page_token,
207        })
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use serde_json::json;
215    use wiremock::matchers::{method, path};
216    use wiremock::{Mock, MockServer, ResponseTemplate};
217
218    async fn operations_against(server: &MockServer) -> (HttpClient, IdentityToolkitEndpoints) {
219        (
220            HttpClient::default(),
221            IdentityToolkitEndpoints::custom(server.uri()),
222        )
223    }
224
225    #[tokio::test]
226    async fn get_user_returns_the_matching_record() {
227        let server = MockServer::start().await;
228        Mock::given(method("POST"))
229            .and(path("/accounts:lookup"))
230            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
231                "users": [{
232                    "localId": "uid-1",
233                    "email": "user@example.com",
234                    "emailVerified": true,
235                }]
236            })))
237            .mount(&server)
238            .await;
239
240        let (http, endpoints) = operations_against(&server).await;
241        let ops = UserOperations::new(&http, &endpoints, Some("token"), None);
242
243        let user = ops.get_user("uid-1").await.unwrap();
244        assert_eq!(user.uid, "uid-1");
245        assert_eq!(user.email.as_deref(), Some("user@example.com"));
246        assert!(user.email_verified);
247    }
248
249    #[tokio::test]
250    async fn get_user_with_no_matches_is_user_not_found() {
251        let server = MockServer::start().await;
252        Mock::given(method("POST"))
253            .and(path("/accounts:lookup"))
254            .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "users": [] })))
255            .mount(&server)
256            .await;
257
258        let (http, endpoints) = operations_against(&server).await;
259        let ops = UserOperations::new(&http, &endpoints, Some("token"), None);
260
261        let err = ops.get_user("missing-uid").await.unwrap_err();
262        assert!(matches!(err, AuthError::UserNotFound));
263    }
264
265    #[tokio::test]
266    async fn create_user_looks_up_the_new_user_after_sign_up() {
267        let server = MockServer::start().await;
268        Mock::given(method("POST"))
269            .and(path("/accounts:signUp"))
270            .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "localId": "new-uid" })))
271            .mount(&server)
272            .await;
273        Mock::given(method("POST"))
274            .and(path("/accounts:lookup"))
275            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
276                "users": [{ "localId": "new-uid", "email": "new@example.com" }]
277            })))
278            .mount(&server)
279            .await;
280
281        let (http, endpoints) = operations_against(&server).await;
282        let ops = UserOperations::new(&http, &endpoints, Some("token"), None);
283
284        let user = ops
285            .create_user(CreateUserRequest {
286                email: Some("new@example.com".to_string()),
287                password: Some("correct horse battery staple".to_string()),
288                ..Default::default()
289            })
290            .await
291            .unwrap();
292        assert_eq!(user.uid, "new-uid");
293    }
294
295    #[tokio::test]
296    async fn delete_user_succeeds_on_a_200_response() {
297        let server = MockServer::start().await;
298        Mock::given(method("POST"))
299            .and(path("/accounts:delete"))
300            .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
301            .mount(&server)
302            .await;
303
304        let (http, endpoints) = operations_against(&server).await;
305        let ops = UserOperations::new(&http, &endpoints, Some("token"), None);
306
307        ops.delete_user("uid-1").await.unwrap();
308    }
309
310    #[tokio::test]
311    async fn list_users_returns_records_and_next_page_token() {
312        let server = MockServer::start().await;
313        Mock::given(method("POST"))
314            .and(path("/accounts:batchGet"))
315            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
316                "users": [
317                    { "localId": "uid-1" },
318                    { "localId": "uid-2" },
319                ],
320                "nextPageToken": "page-2",
321            })))
322            .mount(&server)
323            .await;
324
325        let (http, endpoints) = operations_against(&server).await;
326        let ops = UserOperations::new(&http, &endpoints, Some("token"), None);
327
328        let page = ops.list_users(10, None).await.unwrap();
329        assert_eq!(page.users.len(), 2);
330        assert_eq!(page.next_page_token.as_deref(), Some("page-2"));
331    }
332
333    #[tokio::test]
334    async fn a_structured_api_error_populates_error_code() {
335        let server = MockServer::start().await;
336        Mock::given(method("POST"))
337            .and(path("/accounts:signUp"))
338            .respond_with(ResponseTemplate::new(400).set_body_json(json!({
339                "error": {
340                    "code": 400,
341                    "message": "EMAIL_EXISTS",
342                    "errors": [{ "message": "EMAIL_EXISTS", "reason": "EMAIL_EXISTS" }]
343                }
344            })))
345            .mount(&server)
346            .await;
347
348        let (http, endpoints) = operations_against(&server).await;
349        let ops = UserOperations::new(&http, &endpoints, Some("token"), None);
350
351        let err = ops
352            .create_user(CreateUserRequest {
353                email: Some("taken@example.com".to_string()),
354                password: Some("correct horse battery staple".to_string()),
355                ..Default::default()
356            })
357            .await
358            .unwrap_err();
359
360        match err {
361            AuthError::Api {
362                status, error_code, ..
363            } => {
364                assert_eq!(status, 400);
365                assert_eq!(error_code.as_deref(), Some("EMAIL_EXISTS"));
366            }
367            other => panic!("expected AuthError::Api, got {other:?}"),
368        }
369    }
370}