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.
- package/LICENSE +21 -0
- package/NOTICE +13 -0
- package/README.md +94 -0
- package/dist/CloudFireAuth.d.ts +291 -0
- package/dist/CloudFireAuth.js +346 -0
- package/dist/google-auth/get-oauth-2-token.d.ts +15 -0
- package/dist/google-auth/get-oauth-2-token.js +66 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/rest-api/create-user.d.ts +2 -0
- package/dist/rest-api/create-user.js +3 -0
- package/dist/rest-api/delete-user.d.ts +175 -0
- package/dist/rest-api/delete-user.js +207 -0
- package/dist/rest-api/delete-users.d.ts +2 -0
- package/dist/rest-api/delete-users.js +3 -0
- package/dist/rest-api/get-user-by-email.d.ts +2 -0
- package/dist/rest-api/get-user-by-email.js +3 -0
- package/dist/rest-api/get-user-by-phone-number.d.ts +2 -0
- package/dist/rest-api/get-user-by-phone-number.js +3 -0
- package/dist/rest-api/get-user-by-provider-uid.d.ts +2 -0
- package/dist/rest-api/get-user-by-provider-uid.js +3 -0
- package/dist/rest-api/get-user.d.ts +99 -0
- package/dist/rest-api/get-user.js +177 -0
- package/dist/rest-api/get-users.d.ts +2 -0
- package/dist/rest-api/get-users.js +3 -0
- package/dist/rest-api/list-users.d.ts +2 -0
- package/dist/rest-api/list-users.js +3 -0
- package/dist/rest-api/revoke-refresh-tokens.d.ts +116 -0
- package/dist/rest-api/revoke-refresh-tokens.js +151 -0
- package/dist/rest-api/set-custom-user-claims.d.ts +173 -0
- package/dist/rest-api/set-custom-user-claims.js +261 -0
- package/dist/rest-api/standard-request.d.ts +12 -0
- package/dist/rest-api/standard-request.js +20 -0
- package/dist/rest-api/update-user.d.ts +175 -0
- package/dist/rest-api/update-user.js +375 -0
- package/dist/rest-api/verify-id-token.d.ts +127 -0
- package/dist/rest-api/verify-id-token.js +273 -0
- package/dist/rest-api/verify-session-cookie.d.ts +2 -0
- package/dist/rest-api/verify-session-cookie.js +3 -0
- package/dist/types/firebase-admin/auth-config.d.ts +851 -0
- package/dist/types/firebase-admin/auth-config.js +1 -0
- package/dist/types/firebase-admin/identifier.d.ts +57 -0
- package/dist/types/firebase-admin/identifier.js +1 -0
- package/dist/types/firebase-admin/index.d.ts +153 -0
- package/dist/types/firebase-admin/index.js +1 -0
- package/dist/types/firebase-admin/token-verifier.d.ts +219 -0
- package/dist/types/firebase-admin/token-verifier.js +1 -0
- package/dist/types/firebase-admin/user-record.d.ts +289 -0
- package/dist/types/firebase-admin/user-record.js +1 -0
- package/dist/types/google-auth.d.ts +25 -0
- package/dist/types/google-auth.js +1 -0
- package/dist/types/service-account-key.d.ts +13 -0
- package/dist/types/service-account-key.js +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +1 -0
- package/package.json +56 -0
- 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
|
+
}>;
|