cloudfire-auth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +13 -0
  3. package/README.md +94 -0
  4. package/dist/CloudFireAuth.d.ts +291 -0
  5. package/dist/CloudFireAuth.js +346 -0
  6. package/dist/google-auth/get-oauth-2-token.d.ts +15 -0
  7. package/dist/google-auth/get-oauth-2-token.js +66 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +2 -0
  10. package/dist/rest-api/create-user.d.ts +2 -0
  11. package/dist/rest-api/create-user.js +3 -0
  12. package/dist/rest-api/delete-user.d.ts +175 -0
  13. package/dist/rest-api/delete-user.js +207 -0
  14. package/dist/rest-api/delete-users.d.ts +2 -0
  15. package/dist/rest-api/delete-users.js +3 -0
  16. package/dist/rest-api/get-user-by-email.d.ts +2 -0
  17. package/dist/rest-api/get-user-by-email.js +3 -0
  18. package/dist/rest-api/get-user-by-phone-number.d.ts +2 -0
  19. package/dist/rest-api/get-user-by-phone-number.js +3 -0
  20. package/dist/rest-api/get-user-by-provider-uid.d.ts +2 -0
  21. package/dist/rest-api/get-user-by-provider-uid.js +3 -0
  22. package/dist/rest-api/get-user.d.ts +99 -0
  23. package/dist/rest-api/get-user.js +177 -0
  24. package/dist/rest-api/get-users.d.ts +2 -0
  25. package/dist/rest-api/get-users.js +3 -0
  26. package/dist/rest-api/list-users.d.ts +2 -0
  27. package/dist/rest-api/list-users.js +3 -0
  28. package/dist/rest-api/revoke-refresh-tokens.d.ts +116 -0
  29. package/dist/rest-api/revoke-refresh-tokens.js +151 -0
  30. package/dist/rest-api/set-custom-user-claims.d.ts +173 -0
  31. package/dist/rest-api/set-custom-user-claims.js +261 -0
  32. package/dist/rest-api/standard-request.d.ts +12 -0
  33. package/dist/rest-api/standard-request.js +20 -0
  34. package/dist/rest-api/update-user.d.ts +175 -0
  35. package/dist/rest-api/update-user.js +375 -0
  36. package/dist/rest-api/verify-id-token.d.ts +127 -0
  37. package/dist/rest-api/verify-id-token.js +273 -0
  38. package/dist/rest-api/verify-session-cookie.d.ts +2 -0
  39. package/dist/rest-api/verify-session-cookie.js +3 -0
  40. package/dist/types/firebase-admin/auth-config.d.ts +851 -0
  41. package/dist/types/firebase-admin/auth-config.js +1 -0
  42. package/dist/types/firebase-admin/identifier.d.ts +57 -0
  43. package/dist/types/firebase-admin/identifier.js +1 -0
  44. package/dist/types/firebase-admin/index.d.ts +153 -0
  45. package/dist/types/firebase-admin/index.js +1 -0
  46. package/dist/types/firebase-admin/token-verifier.d.ts +219 -0
  47. package/dist/types/firebase-admin/token-verifier.js +1 -0
  48. package/dist/types/firebase-admin/user-record.d.ts +289 -0
  49. package/dist/types/firebase-admin/user-record.js +1 -0
  50. package/dist/types/google-auth.d.ts +25 -0
  51. package/dist/types/google-auth.js +1 -0
  52. package/dist/types/service-account-key.d.ts +13 -0
  53. package/dist/types/service-account-key.js +1 -0
  54. package/dist/types.d.ts +6 -0
  55. package/dist/types.js +1 -0
  56. package/package.json +56 -0
  57. package/third_party/firebase-admin-license.txt +201 -0
@@ -0,0 +1,175 @@
1
+ import type { UpdateRequest, UserRecord } from "../types.js";
2
+ /**
3
+ * Updates an existing Firebase Auth user with the specified properties and returns the complete updated user record.
4
+ *
5
+ * This function provides comprehensive user management capabilities through a two-step process:
6
+ * 1. **Validation & Update**: Validates the update request and calls Firebase Admin API's accounts:update endpoint
7
+ * 2. **Data Retrieval**: Fetches the complete updated user record using getUserHandler for consistency
8
+ *
9
+ * This approach ensures you receive a complete, properly formatted UserRecord with all user data
10
+ * including custom claims, metadata, provider information, and any computed fields.
11
+ *
12
+ * **Supported Update Operations:**
13
+ * - **Profile Information**: displayName, photoURL
14
+ * - **Authentication**: email, emailVerified, password, phoneNumber
15
+ * - **Access Control**: disabled status
16
+ * - **Multi-Factor Authentication**: MFA settings and enrolled factors
17
+ * - **Provider Management**: link/unlink identity providers
18
+ *
19
+ * **Validation Rules:**
20
+ * - Only allowed properties are accepted (rejects unknown fields)
21
+ * - Type validation for all properties (boolean, string, object, array)
22
+ * - Email format validation using regex pattern
23
+ * - Password minimum length requirement (6 characters)
24
+ * - URL validation for photoURL using URL constructor
25
+ * - Null values allowed for clearable fields (displayName, phoneNumber, photoURL)
26
+ *
27
+ * **Firebase API Integration:**
28
+ * - **Step 1**: Uses Firebase Admin API's `accounts:update` endpoint to apply changes
29
+ * - Transforms fields to Firebase API format automatically
30
+ * - `providerToLink` becomes `linkProviderUserInfo`
31
+ * - `providersToUnlink` becomes `deleteProvider`
32
+ * - `photoURL` becomes `photoUrl` (lowercase 'u')
33
+ * - Validates update response for consistency
34
+ * - **Step 2**: Uses `getUserHandler` to retrieve complete updated user data
35
+ * - Ensures consistent UserRecord format across the application
36
+ * - Includes all user metadata, custom claims, and provider information
37
+ * - Handles complex data transformations automatically
38
+ *
39
+ * @param uid - The Firebase Auth user ID (localId) to update.
40
+ * Must be a valid, existing Firebase user identifier.
41
+ * @param properties - The properties to update for the user.
42
+ * Must contain valid UpdateRequest fields only.
43
+ * @param oauth2AccessToken - Valid OAuth2 access token with Firebase Admin API privileges.
44
+ * Obtained via service account authentication.
45
+ *
46
+ * @returns Promise that resolves to the complete updated UserRecord with all user data.
47
+ * The returned UserRecord includes updated fields plus all existing user information
48
+ * such as metadata, custom claims, provider data, and computed fields.
49
+ *
50
+ * @throws {Error} When validation, update, or retrieval operations fail:
51
+ * - **Validation Errors**:
52
+ * - "Invalid properties provided: {props}" - Unknown properties in request
53
+ * - "{field} must be a {type}" - Type validation failures
54
+ * - "Invalid email format" - Email doesn't match regex pattern
55
+ * - "password must be at least 6 characters long" - Password too short
56
+ * - "photoURL must be a valid URL" - URL validation failed
57
+ * - **Update API Errors**:
58
+ * - "Failed to update user: {status} {statusText}\n{details}" - Firebase API errors with detailed error information
59
+ * - "Invalid response from Firebase API - user ID mismatch" - Unexpected response format
60
+ * - **Retrieval Errors**:
61
+ * - "User updated successfully, but failed to retrieve updated data: {reason}" - Update succeeded but data retrieval failed
62
+ * - **Network Errors**: Various network-related failures during API communication
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * // Update basic profile information
67
+ * const updatedUser = await updateUserHandler(
68
+ * 'user123',
69
+ * {
70
+ * displayName: 'John Doe',
71
+ * photoURL: 'https://example.com/photo.jpg'
72
+ * },
73
+ * oauth2Token
74
+ * );
75
+ *
76
+ * console.log('Profile updated:', updatedUser.displayName); // "John Doe"
77
+ * console.log('Photo URL:', updatedUser.photoURL); // "https://example.com/photo.jpg"
78
+ * console.log('User metadata:', updatedUser.metadata); // Includes creation time, last sign-in, etc.
79
+ * ```
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * // Update authentication credentials
84
+ * const userWithNewEmail = await updateUserHandler(
85
+ * 'user456',
86
+ * {
87
+ * email: 'newemail@example.com',
88
+ * emailVerified: false,
89
+ * password: 'newSecurePassword123'
90
+ * },
91
+ * oauth2Token
92
+ * );
93
+ *
94
+ * console.log('Email updated:', userWithNewEmail.email); // "newemail@example.com"
95
+ * console.log('Email verified:', userWithNewEmail.emailVerified); // false
96
+ * console.log('Provider data:', userWithNewEmail.providerData); // All linked providers
97
+ * ```
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * // Clear optional fields with null and manage providers
102
+ * const userWithProviderChanges = await updateUserHandler(
103
+ * 'user789',
104
+ * {
105
+ * displayName: null, // Clear display name
106
+ * phoneNumber: null, // Clear phone number
107
+ * providersToUnlink: ['facebook.com'], // Remove Facebook provider
108
+ * providerToLink: { // Link new Google provider
109
+ * providerId: 'google.com',
110
+ * uid: 'google-uid-12345'
111
+ * }
112
+ * },
113
+ * oauth2Token
114
+ * );
115
+ *
116
+ * console.log('Display name cleared:', userWithProviderChanges.displayName); // null
117
+ * console.log('Updated providers:', userWithProviderChanges.providerData); // Reflects provider changes
118
+ * ```
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * // Comprehensive error handling
123
+ * try {
124
+ * const updatedUser = await updateUserHandler(userId, updates, oauth2Token);
125
+ * console.log('User updated successfully:', updatedUser.uid);
126
+ *
127
+ * // The returned user includes all data - you can access any field
128
+ * console.log('Custom claims:', updatedUser.customClaims);
129
+ * console.log('Last sign-in:', updatedUser.metadata.lastSignInTime);
130
+ * console.log('All providers:', updatedUser.providerData);
131
+ *
132
+ * } catch (error) {
133
+ * if (error.message.includes('Invalid properties provided')) {
134
+ * console.error('Request validation failed:', error.message);
135
+ * } else if (error.message.includes('Failed to update user')) {
136
+ * console.error('Firebase update API error:', error.message);
137
+ * } else if (error.message.includes('User updated successfully, but failed to retrieve')) {
138
+ * console.error('Update succeeded but data retrieval failed:', error.message);
139
+ * // User was updated, but we couldn't get the fresh data
140
+ * } else {
141
+ * console.error('Unexpected error:', error);
142
+ * }
143
+ * }
144
+ * ```
145
+ *
146
+ * **Important Implementation Notes:**
147
+ * - **Two-Step Process**: Function performs update then retrieval for complete data consistency
148
+ * - **Atomic Updates**: Firebase update operations are atomic - either all succeed or none are applied
149
+ * - **Complete Data**: Always returns full UserRecord with metadata, claims, providers, etc.
150
+ * - **Consistency**: Uses same data formatting as getUserHandler for uniform API responses
151
+ * - **Error Recovery**: Clear error messages distinguish between update and retrieval failures
152
+ * - **Field Transformations**: Automatically handles Firebase API format differences (providers, photoURL)
153
+ * - **Session Impact**: Password updates may invalidate existing user sessions
154
+ * - **Email Verification**: Email updates should be followed by verification flows
155
+ * - **Performance**: Makes two API calls but ensures data completeness and consistency
156
+ *
157
+ * **Security Considerations:**
158
+ * - Requires Firebase Admin API privileges via OAuth2 token
159
+ * - Validates all input properties before making API calls
160
+ * - Disabled users cannot sign in until re-enabled
161
+ * - Provider operations affect available authentication methods
162
+ * - Phone number validation is currently minimal (see TODO in validation)
163
+ *
164
+ * TODO: It appears there are still parts that require implementation:
165
+ * https://github.com/firebase/firebase-admin-node/blob/master/src/auth/auth-api-request.ts#L1371
166
+ *
167
+ * @see {@link checkUpdateUserRequest} For detailed validation rules and allowed properties
168
+ * @see {@link getUserHandler} For the data retrieval implementation used in step 2
169
+ * @see {@link https://firebase.google.com/docs/auth/admin/manage-users Firebase User Management}
170
+ * @see {@link https://firebase.google.com/docs/reference/rest/auth#section-update-account Firebase REST API}
171
+ *
172
+ * @package
173
+ * @since 1.0.0
174
+ */
175
+ export declare function updateUserHandler(uid: string, properties: UpdateRequest, oauth2AccessToken: string): Promise<UserRecord>;
@@ -0,0 +1,375 @@
1
+ import { getUserHandler } from "./get-user.js";
2
+ const deletableAttributes = {
3
+ displayName: "DISPLAY_NAME",
4
+ photoURL: "PHOTO_URL",
5
+ // TODO: Decide if these should be deletable too, they're not in firebase admin SDK
6
+ // You can see the attributes they allow you to delete here:
7
+ // https://github.com/firebase/firebase-admin-node/blob/master/src/auth/auth-api-request.ts#L1420
8
+ // email: "EMAIL",
9
+ // phoneNumber: "PHONE_NUMBER",
10
+ // provider: "PROVIDER",
11
+ // password: "PASSWORD",
12
+ // rawUserInfo: "RAW_USER_INFO",
13
+ };
14
+ /**
15
+ * Updates an existing Firebase Auth user with the specified properties and returns the complete updated user record.
16
+ *
17
+ * This function provides comprehensive user management capabilities through a two-step process:
18
+ * 1. **Validation & Update**: Validates the update request and calls Firebase Admin API's accounts:update endpoint
19
+ * 2. **Data Retrieval**: Fetches the complete updated user record using getUserHandler for consistency
20
+ *
21
+ * This approach ensures you receive a complete, properly formatted UserRecord with all user data
22
+ * including custom claims, metadata, provider information, and any computed fields.
23
+ *
24
+ * **Supported Update Operations:**
25
+ * - **Profile Information**: displayName, photoURL
26
+ * - **Authentication**: email, emailVerified, password, phoneNumber
27
+ * - **Access Control**: disabled status
28
+ * - **Multi-Factor Authentication**: MFA settings and enrolled factors
29
+ * - **Provider Management**: link/unlink identity providers
30
+ *
31
+ * **Validation Rules:**
32
+ * - Only allowed properties are accepted (rejects unknown fields)
33
+ * - Type validation for all properties (boolean, string, object, array)
34
+ * - Email format validation using regex pattern
35
+ * - Password minimum length requirement (6 characters)
36
+ * - URL validation for photoURL using URL constructor
37
+ * - Null values allowed for clearable fields (displayName, phoneNumber, photoURL)
38
+ *
39
+ * **Firebase API Integration:**
40
+ * - **Step 1**: Uses Firebase Admin API's `accounts:update` endpoint to apply changes
41
+ * - Transforms fields to Firebase API format automatically
42
+ * - `providerToLink` becomes `linkProviderUserInfo`
43
+ * - `providersToUnlink` becomes `deleteProvider`
44
+ * - `photoURL` becomes `photoUrl` (lowercase 'u')
45
+ * - Validates update response for consistency
46
+ * - **Step 2**: Uses `getUserHandler` to retrieve complete updated user data
47
+ * - Ensures consistent UserRecord format across the application
48
+ * - Includes all user metadata, custom claims, and provider information
49
+ * - Handles complex data transformations automatically
50
+ *
51
+ * @param uid - The Firebase Auth user ID (localId) to update.
52
+ * Must be a valid, existing Firebase user identifier.
53
+ * @param properties - The properties to update for the user.
54
+ * Must contain valid UpdateRequest fields only.
55
+ * @param oauth2AccessToken - Valid OAuth2 access token with Firebase Admin API privileges.
56
+ * Obtained via service account authentication.
57
+ *
58
+ * @returns Promise that resolves to the complete updated UserRecord with all user data.
59
+ * The returned UserRecord includes updated fields plus all existing user information
60
+ * such as metadata, custom claims, provider data, and computed fields.
61
+ *
62
+ * @throws {Error} When validation, update, or retrieval operations fail:
63
+ * - **Validation Errors**:
64
+ * - "Invalid properties provided: {props}" - Unknown properties in request
65
+ * - "{field} must be a {type}" - Type validation failures
66
+ * - "Invalid email format" - Email doesn't match regex pattern
67
+ * - "password must be at least 6 characters long" - Password too short
68
+ * - "photoURL must be a valid URL" - URL validation failed
69
+ * - **Update API Errors**:
70
+ * - "Failed to update user: {status} {statusText}\n{details}" - Firebase API errors with detailed error information
71
+ * - "Invalid response from Firebase API - user ID mismatch" - Unexpected response format
72
+ * - **Retrieval Errors**:
73
+ * - "User updated successfully, but failed to retrieve updated data: {reason}" - Update succeeded but data retrieval failed
74
+ * - **Network Errors**: Various network-related failures during API communication
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * // Update basic profile information
79
+ * const updatedUser = await updateUserHandler(
80
+ * 'user123',
81
+ * {
82
+ * displayName: 'John Doe',
83
+ * photoURL: 'https://example.com/photo.jpg'
84
+ * },
85
+ * oauth2Token
86
+ * );
87
+ *
88
+ * console.log('Profile updated:', updatedUser.displayName); // "John Doe"
89
+ * console.log('Photo URL:', updatedUser.photoURL); // "https://example.com/photo.jpg"
90
+ * console.log('User metadata:', updatedUser.metadata); // Includes creation time, last sign-in, etc.
91
+ * ```
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * // Update authentication credentials
96
+ * const userWithNewEmail = await updateUserHandler(
97
+ * 'user456',
98
+ * {
99
+ * email: 'newemail@example.com',
100
+ * emailVerified: false,
101
+ * password: 'newSecurePassword123'
102
+ * },
103
+ * oauth2Token
104
+ * );
105
+ *
106
+ * console.log('Email updated:', userWithNewEmail.email); // "newemail@example.com"
107
+ * console.log('Email verified:', userWithNewEmail.emailVerified); // false
108
+ * console.log('Provider data:', userWithNewEmail.providerData); // All linked providers
109
+ * ```
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * // Clear optional fields with null and manage providers
114
+ * const userWithProviderChanges = await updateUserHandler(
115
+ * 'user789',
116
+ * {
117
+ * displayName: null, // Clear display name
118
+ * phoneNumber: null, // Clear phone number
119
+ * providersToUnlink: ['facebook.com'], // Remove Facebook provider
120
+ * providerToLink: { // Link new Google provider
121
+ * providerId: 'google.com',
122
+ * uid: 'google-uid-12345'
123
+ * }
124
+ * },
125
+ * oauth2Token
126
+ * );
127
+ *
128
+ * console.log('Display name cleared:', userWithProviderChanges.displayName); // null
129
+ * console.log('Updated providers:', userWithProviderChanges.providerData); // Reflects provider changes
130
+ * ```
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * // Comprehensive error handling
135
+ * try {
136
+ * const updatedUser = await updateUserHandler(userId, updates, oauth2Token);
137
+ * console.log('User updated successfully:', updatedUser.uid);
138
+ *
139
+ * // The returned user includes all data - you can access any field
140
+ * console.log('Custom claims:', updatedUser.customClaims);
141
+ * console.log('Last sign-in:', updatedUser.metadata.lastSignInTime);
142
+ * console.log('All providers:', updatedUser.providerData);
143
+ *
144
+ * } catch (error) {
145
+ * if (error.message.includes('Invalid properties provided')) {
146
+ * console.error('Request validation failed:', error.message);
147
+ * } else if (error.message.includes('Failed to update user')) {
148
+ * console.error('Firebase update API error:', error.message);
149
+ * } else if (error.message.includes('User updated successfully, but failed to retrieve')) {
150
+ * console.error('Update succeeded but data retrieval failed:', error.message);
151
+ * // User was updated, but we couldn't get the fresh data
152
+ * } else {
153
+ * console.error('Unexpected error:', error);
154
+ * }
155
+ * }
156
+ * ```
157
+ *
158
+ * **Important Implementation Notes:**
159
+ * - **Two-Step Process**: Function performs update then retrieval for complete data consistency
160
+ * - **Atomic Updates**: Firebase update operations are atomic - either all succeed or none are applied
161
+ * - **Complete Data**: Always returns full UserRecord with metadata, claims, providers, etc.
162
+ * - **Consistency**: Uses same data formatting as getUserHandler for uniform API responses
163
+ * - **Error Recovery**: Clear error messages distinguish between update and retrieval failures
164
+ * - **Field Transformations**: Automatically handles Firebase API format differences (providers, photoURL)
165
+ * - **Session Impact**: Password updates may invalidate existing user sessions
166
+ * - **Email Verification**: Email updates should be followed by verification flows
167
+ * - **Performance**: Makes two API calls but ensures data completeness and consistency
168
+ *
169
+ * **Security Considerations:**
170
+ * - Requires Firebase Admin API privileges via OAuth2 token
171
+ * - Validates all input properties before making API calls
172
+ * - Disabled users cannot sign in until re-enabled
173
+ * - Provider operations affect available authentication methods
174
+ * - Phone number validation is currently minimal (see TODO in validation)
175
+ *
176
+ * TODO: It appears there are still parts that require implementation:
177
+ * https://github.com/firebase/firebase-admin-node/blob/master/src/auth/auth-api-request.ts#L1371
178
+ *
179
+ * @see {@link checkUpdateUserRequest} For detailed validation rules and allowed properties
180
+ * @see {@link getUserHandler} For the data retrieval implementation used in step 2
181
+ * @see {@link https://firebase.google.com/docs/auth/admin/manage-users Firebase User Management}
182
+ * @see {@link https://firebase.google.com/docs/reference/rest/auth#section-update-account Firebase REST API}
183
+ *
184
+ * @package
185
+ * @since 1.0.0
186
+ */
187
+ export async function updateUserHandler(uid, properties, oauth2AccessToken) {
188
+ const validProperties = checkUpdateUserRequest(properties);
189
+ if (typeof uid !== "string" || uid.length === 0) {
190
+ throw new Error("uid must be a non-empty string, got: " + uid);
191
+ }
192
+ // Transform provider operations to Firebase API format
193
+ const requestBody = {
194
+ ...validProperties,
195
+ localId: uid,
196
+ };
197
+ // Handle deletable attributes
198
+ requestBody.deleteAttribute = [];
199
+ for (const key in deletableAttributes) {
200
+ if (validProperties[key] === null) {
201
+ requestBody.deleteAttribute.push(deletableAttributes[key]);
202
+ delete requestBody[key];
203
+ }
204
+ }
205
+ // Remove deleteAttribute if it's empty
206
+ if (requestBody.deleteAttribute.length === 0) {
207
+ delete requestBody.deleteAttribute;
208
+ }
209
+ // Handle provider linking transformation
210
+ if (validProperties.providerToLink) {
211
+ requestBody.linkProviderUserInfo = validProperties.providerToLink;
212
+ delete requestBody.providerToLink;
213
+ }
214
+ // Handle provider unlinking transformation
215
+ if (validProperties.providersToUnlink) {
216
+ requestBody.deleteProvider = validProperties.providersToUnlink;
217
+ delete requestBody.providersToUnlink;
218
+ }
219
+ // Transform photoURL to photoUrl for Firebase API
220
+ if (requestBody.photoURL !== undefined) {
221
+ requestBody.photoUrl = requestBody.photoURL;
222
+ delete requestBody.photoURL;
223
+ }
224
+ // Transform disabled to disableUser
225
+ if (validProperties.disabled !== undefined) {
226
+ requestBody.disableUser = validProperties.disabled;
227
+ delete requestBody.disabled;
228
+ }
229
+ // Transform phoneNumber into a provider deletion if it's set to null
230
+ if (requestBody.phoneNumber === null) {
231
+ requestBody.deleteProvider ? requestBody.deleteProvider.push("phone") : (requestBody.deleteProvider = ["phone"]);
232
+ delete requestBody.phoneNumber;
233
+ }
234
+ const response = await fetch("https://identitytoolkit.googleapis.com/v1/accounts:update", {
235
+ method: "POST",
236
+ headers: {
237
+ "Content-Type": "application/json",
238
+ Authorization: `Bearer ${oauth2AccessToken}`,
239
+ },
240
+ body: JSON.stringify(requestBody),
241
+ });
242
+ if (!response.ok) {
243
+ const errorText = await response.text();
244
+ let errorMessage;
245
+ try {
246
+ const errorData = JSON.parse(errorText);
247
+ const formattedErrorText = JSON.stringify(errorData, null, 2);
248
+ errorMessage = `Failed to update user: ${response.status} ${response.statusText}\n${formattedErrorText}`;
249
+ }
250
+ catch {
251
+ errorMessage = `Failed to update user: ${response.status} ${response.statusText} - ${errorText}`;
252
+ }
253
+ throw new Error(errorMessage);
254
+ }
255
+ const data = (await response.json());
256
+ if (data.localId !== uid) {
257
+ throw new Error("Invalid response from Firebase API - user ID mismatch");
258
+ }
259
+ // Retrieve the complete updated user record
260
+ try {
261
+ const updatedUserRecord = await getUserHandler(uid, oauth2AccessToken);
262
+ return updatedUserRecord;
263
+ }
264
+ catch (error) {
265
+ throw new Error(`User updated successfully, but failed to retrieve updated data: ${error.message}`);
266
+ }
267
+ }
268
+ /**
269
+ * Checks the update user request had valid properties. Meaning it checks
270
+ * the keys on the properties object are allowed and the values are of
271
+ * the correct type. If any of the properties are invalid, an error is thrown.
272
+ *
273
+ * @throws {Error} If any of the properties are invalid.
274
+ *
275
+ * @param {UpdateRequest} properties The properties provided to update the user.
276
+ * @return {UpdateRequest} The properties object with the valid properties.
277
+ */
278
+ function checkUpdateUserRequest(properties) {
279
+ const allowedProperties = [
280
+ "disabled",
281
+ "displayName",
282
+ "email",
283
+ "emailVerified",
284
+ "password",
285
+ "phoneNumber",
286
+ "photoURL",
287
+ "multiFactor",
288
+ "providerToLink",
289
+ "providersToUnlink",
290
+ ];
291
+ if (Object.keys(properties).length === 0) {
292
+ throw new Error("Request body is empty. Please provide at least one property to update.");
293
+ }
294
+ // Check that only allowed properties are present
295
+ const providedProperties = Object.keys(properties);
296
+ const invalidProperties = providedProperties.filter((prop) => !allowedProperties.includes(prop));
297
+ if (invalidProperties.length > 0) {
298
+ throw new Error(`Invalid properties provided: ${invalidProperties.join(", ")}`);
299
+ }
300
+ // Validate each field if present
301
+ if (properties.disabled !== undefined && typeof properties.disabled !== "boolean") {
302
+ throw new Error("disabled must be a boolean");
303
+ }
304
+ if (properties.displayName !== undefined &&
305
+ properties.displayName !== null &&
306
+ typeof properties.displayName !== "string") {
307
+ throw new Error("displayName must be a string or null");
308
+ }
309
+ if (properties.email !== undefined) {
310
+ if (typeof properties.email !== "string") {
311
+ throw new Error("email must be a string");
312
+ }
313
+ // Basic email validation
314
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
315
+ if (!emailRegex.test(properties.email)) {
316
+ throw new Error("Invalid email format");
317
+ }
318
+ }
319
+ if (properties.emailVerified !== undefined && typeof properties.emailVerified !== "boolean") {
320
+ throw new Error("emailVerified must be a boolean");
321
+ }
322
+ if (properties.password !== undefined) {
323
+ if (typeof properties.password !== "string") {
324
+ throw new Error("password must be a string");
325
+ }
326
+ if (properties.password.length < 6) {
327
+ throw new Error("password must be at least 6 characters long");
328
+ }
329
+ }
330
+ if (properties.phoneNumber !== undefined && properties.phoneNumber !== null) {
331
+ if (typeof properties.phoneNumber !== "string") {
332
+ throw new Error("phoneNumber must be a string or null");
333
+ }
334
+ // TODO: Decide if phone number validation is needed
335
+ // // Basic phone number validation (E.164 format)
336
+ // const phoneRegex = /^\+[1-9]\d{1,14}$/;
337
+ // if (!phoneRegex.test(properties.phoneNumber)) {
338
+ // throw new Error('phoneNumber must be in E.164 format (e.g., +16505550123)');
339
+ // }
340
+ }
341
+ if (properties.photoURL !== undefined && properties.photoURL !== null) {
342
+ if (typeof properties.photoURL !== "string") {
343
+ throw new Error("photoURL must be a string or null");
344
+ }
345
+ try {
346
+ new URL(properties.photoURL);
347
+ }
348
+ catch {
349
+ throw new Error("photoURL must be a valid URL");
350
+ }
351
+ }
352
+ if (properties.multiFactor !== undefined) {
353
+ if (properties.multiFactor !== null && typeof properties.multiFactor !== "object") {
354
+ throw new Error("multiFactor must be an object or null");
355
+ }
356
+ if (properties.multiFactor?.enrolledFactors !== undefined &&
357
+ properties.multiFactor.enrolledFactors !== null &&
358
+ !Array.isArray(properties.multiFactor.enrolledFactors)) {
359
+ throw new Error("multiFactor.enrolledFactors must be an array or null");
360
+ }
361
+ }
362
+ if (properties.providerToLink !== undefined &&
363
+ (typeof properties.providerToLink !== "object" || properties.providerToLink === null)) {
364
+ throw new Error("providerToLink must be a UserProvider object");
365
+ }
366
+ if (properties.providersToUnlink !== undefined) {
367
+ if (!Array.isArray(properties.providersToUnlink)) {
368
+ throw new Error("providersToUnlink must be an array");
369
+ }
370
+ if (!properties.providersToUnlink.every((provider) => typeof provider === "string")) {
371
+ throw new Error("all providers in providersToUnlink must be strings");
372
+ }
373
+ }
374
+ return properties;
375
+ }
@@ -0,0 +1,127 @@
1
+ import type { DecodedIdToken } from "../types.js";
2
+ /**
3
+ * Verifies a Firebase ID token (JWT) and returns its decoded claims.
4
+ *
5
+ * This function performs comprehensive validation of Firebase ID tokens including:
6
+ * - JWT structure and format validation
7
+ * - Cryptographic signature verification using Google's public keys
8
+ * - Firebase-specific claim validation (audience, issuer, timing)
9
+ * - Optional revocation status checking
10
+ *
11
+ * The verification process follows Firebase's recommended security practices:
12
+ * 1. Validates JWT body claims (aud, iss, sub, exp, iat, auth_time)
13
+ * 2. Validates JWT header (algorithm RS256, key ID)
14
+ * 3. Fetches and caches Google's signing keys from their public API
15
+ * 4. Verifies cryptographic signature using the appropriate public key
16
+ * 5. Optionally checks if the user's tokens have been revoked
17
+ *
18
+ * **Key Features:**
19
+ * - Automatic public key fetching and caching (when KV namespace provided)
20
+ * - Proper error handling with descriptive error messages
21
+ * - Support for revocation checking via Firebase Admin API
22
+ * - Network error resilience
23
+ *
24
+ * **Security Validations:**
25
+ * - Ensures token audience matches the provided project ID
26
+ * - Verifies token was issued by Firebase (`https://securetoken.google.com/{projectId}`)
27
+ * - Checks token hasn't expired and wasn't issued in the future
28
+ * - Validates authentication time is not in the future
29
+ * - Ensures subject (user ID) is a non-empty string
30
+ * - Confirms token uses RS256 algorithm (not vulnerable algorithms)
31
+ *
32
+ * **Performance Optimizations:**
33
+ * - Caches Google public keys in KV storage to avoid repeated API calls
34
+ * - Respects cache headers from Google's key endpoint
35
+ * - Fails fast on basic validation errors before expensive crypto operations
36
+ *
37
+ * @param idToken - The Firebase ID token (JWT) to verify. Must be a valid JWT string.
38
+ * @param projectId - Your Firebase project ID. Used to validate token audience and issuer.
39
+ * @param oauth2Token - OAuth2 access token for Firebase Admin API. Required for revocation checks.
40
+ * @param kv - Optional Cloudflare KV namespace for caching Google's public keys.
41
+ * Improves performance by avoiding repeated key fetches.
42
+ * @param checkRevoked - Whether to check if the token has been revoked by comparing
43
+ * against the user's `tokensValidAfterTime`. Requires an additional
44
+ * API call to Firebase Admin API.
45
+ *
46
+ * @returns A Promise that resolves to the decoded ID token containing user claims
47
+ * and Firebase-specific metadata. The returned object includes standard
48
+ * JWT claims (iss, aud, exp, iat, sub) plus Firebase-specific claims
49
+ * (email, email_verified, firebase, etc.).
50
+ *
51
+ * @throws {Error} When token validation fails:
52
+ * - "Token audience does not match project ID" - Wrong project ID
53
+ * - "Token issuer does not match project ID" - Not issued by Firebase
54
+ * - "Token expiration date is in the past" - Expired token
55
+ * - "Token issued at date is in the future" - Clock skew or forged token
56
+ * - "Token subject is empty" - Missing or invalid user ID
57
+ * - "Token algorithm is not RS256" - Unsafe algorithm
58
+ * - "Token key ID is not in the Google API" - Unknown or rotated key
59
+ * - "Token is invalid" - Signature verification failed
60
+ * - "Token is revoked" - User tokens were revoked (when checkRevoked=true)
61
+ * - Network errors when fetching Google's public keys or checking revocation
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * // Basic token verification
66
+ * const decodedToken = await verifyIdTokenHandler(
67
+ * idToken,
68
+ * "my-project-id",
69
+ * oauth2Token
70
+ * );
71
+ * console.log("User ID:", decodedToken.uid);
72
+ * console.log("Email:", decodedToken.email);
73
+ *
74
+ * // With caching and revocation checking
75
+ * const decodedToken = await verifyIdTokenHandler(
76
+ * idToken,
77
+ * "my-project-id",
78
+ * oauth2Token,
79
+ * kvNamespace, // Enables key caching
80
+ * true // Check if token was revoked
81
+ * );
82
+ * ```
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * // Error handling
87
+ * try {
88
+ * const decodedToken = await verifyIdTokenHandler(idToken, projectId, oauth2Token);
89
+ * // Token is valid, proceed with authenticated request
90
+ * return processAuthenticatedRequest(decodedToken);
91
+ * } catch (error) {
92
+ * if (error.message.includes('expired')) {
93
+ * return { error: 'Please log in again' };
94
+ * } else if (error.message.includes('revoked')) {
95
+ * return { error: 'Access has been revoked' };
96
+ * } else {
97
+ * return { error: 'Invalid token' };
98
+ * }
99
+ * }
100
+ * ```
101
+ *
102
+ * @see {@link https://firebase.google.com/docs/auth/admin/verify-id-tokens Firebase Admin SDK Token Verification}
103
+ * @see {@link https://tools.ietf.org/html/rfc7519 JWT RFC 7519}
104
+ * @see {@link https://firebase.google.com/docs/reference/admin/node/firebase-admin.auth.auth.md#authverifyidtoken Firebase verifyIdToken Reference}
105
+ *
106
+ * @since 1.0.0
107
+ * @package
108
+ */
109
+ export declare function verifyIdTokenHandler(idToken: string, projectId: string, oauth2Token: string, kv?: KVNamespace, checkRevoked?: boolean): Promise<DecodedIdToken>;
110
+ /**
111
+ * Validates the header of a Firebase JWT. First checks the algorithm type is
112
+ * the expected type, then checks the key ID is valid either by checking in the
113
+ * KV namespace or by fetching the key from the Google API.
114
+ *
115
+ * If a new key is fetched from the Google API that was used to sign the token
116
+ * then it is stored in the KV namespace for the duration of the cache time
117
+ * returned from the Google API in the headers.
118
+ *
119
+ * @param token - The JWT to validate.
120
+ * @param kv - The KV namespace to get the Google public keys from.
121
+ * @returns True and the signing key if the token is valid, false and an error message if the token is invalid.
122
+ */
123
+ export declare function validateJwtHeader(token: string, kv?: KVNamespace): Promise<{
124
+ isValid: boolean;
125
+ errorMessage?: string;
126
+ signingKey?: string;
127
+ }>;