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