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,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revokes all refresh tokens for an existing Firebase Auth user.
|
|
3
|
+
*
|
|
4
|
+
* This function updates the user's `tokensValidAfterTime` to the current UTC timestamp,
|
|
5
|
+
* effectively invalidating all existing refresh tokens and sessions for the specified user.
|
|
6
|
+
* After calling this function, the user will need to re-authenticate to obtain new tokens.
|
|
7
|
+
*
|
|
8
|
+
* **Important Implementation Details:**
|
|
9
|
+
* - Updates the user's `tokensValidAfterTime` to the current UTC timestamp (in seconds)
|
|
10
|
+
* - All existing refresh tokens become invalid immediately
|
|
11
|
+
* - Existing ID tokens remain valid until their natural expiration (1 hour) unless verified with `checkRevoked=true`
|
|
12
|
+
* - Requires accurate server time synchronization for proper functionality
|
|
13
|
+
* - Uses Firebase Admin API's `accounts:update` endpoint with `validSince` parameter
|
|
14
|
+
*
|
|
15
|
+
* **Security Considerations:**
|
|
16
|
+
* - Forces user re-authentication on all devices and sessions
|
|
17
|
+
* - Useful for compromised accounts or mandatory sign-out scenarios
|
|
18
|
+
* - Should be combined with ID token verification using `checkRevoked=true` for immediate effect
|
|
19
|
+
* - Does not affect the user's account data or profile information
|
|
20
|
+
*
|
|
21
|
+
* **Use Cases:**
|
|
22
|
+
* - Security incident response (compromised accounts)
|
|
23
|
+
* - Forced logout from all devices
|
|
24
|
+
* - Administrative account suspension workflows
|
|
25
|
+
* - Password change security measures
|
|
26
|
+
* - Device management and session control
|
|
27
|
+
*
|
|
28
|
+
* @param uid - The Firebase Auth user ID (localId) whose refresh tokens should be revoked.
|
|
29
|
+
* Must be a valid, existing Firebase user identifier.
|
|
30
|
+
* @param oauth2AccessToken - Valid OAuth2 access token with Firebase Admin API privileges.
|
|
31
|
+
* Obtained via service account authentication.
|
|
32
|
+
*
|
|
33
|
+
* @returns Promise that resolves when the revocation is complete.
|
|
34
|
+
* No return value - success is indicated by promise resolution.
|
|
35
|
+
*
|
|
36
|
+
* @throws {Error} When revocation fails:
|
|
37
|
+
* - **Validation Errors**:
|
|
38
|
+
* - "uid must be a non-empty string" - Invalid or missing uid parameter
|
|
39
|
+
* - **Firebase API Errors**:
|
|
40
|
+
* - "Failed to revoke refresh tokens: {status} {statusText} - {details}" - API errors with detailed information
|
|
41
|
+
* - "USER_NOT_FOUND" - Specified user does not exist
|
|
42
|
+
* - "INVALID_ID_TOKEN" - OAuth2 token is invalid or expired
|
|
43
|
+
* - **Network Errors**: Various network-related failures during API communication
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* // Revoke all refresh tokens for a user (security incident)
|
|
48
|
+
* try {
|
|
49
|
+
* await revokeRefreshTokensHandler('user123', oauth2Token);
|
|
50
|
+
* console.log('All refresh tokens revoked successfully');
|
|
51
|
+
*
|
|
52
|
+
* // Verify that existing ID tokens are now invalid
|
|
53
|
+
* await verifyIdTokenHandler(existingIdToken, projectId, oauth2Token, kvNamespace, true);
|
|
54
|
+
* } catch (error) {
|
|
55
|
+
* if (error.message.includes('id-token-revoked')) {
|
|
56
|
+
* console.log('Tokens successfully revoked - existing ID token is invalid');
|
|
57
|
+
* } else {
|
|
58
|
+
* console.error('Failed to revoke tokens:', error);
|
|
59
|
+
* }
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* // Force logout from all devices after password change
|
|
66
|
+
* try {
|
|
67
|
+
* // First update the password
|
|
68
|
+
* await updateUserHandler('user456', { password: 'newSecurePassword' }, oauth2Token);
|
|
69
|
+
*
|
|
70
|
+
* // Then revoke all existing sessions for security
|
|
71
|
+
* await revokeRefreshTokensHandler('user456', oauth2Token);
|
|
72
|
+
*
|
|
73
|
+
* console.log('Password updated and all sessions revoked');
|
|
74
|
+
* } catch (error) {
|
|
75
|
+
* console.error('Security update failed:', error);
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* // Administrative account suspension workflow
|
|
82
|
+
* try {
|
|
83
|
+
* // Disable the account
|
|
84
|
+
* await updateUserHandler('suspendedUser', { disabled: true }, oauth2Token);
|
|
85
|
+
*
|
|
86
|
+
* // Revoke all active sessions
|
|
87
|
+
* await revokeRefreshTokensHandler('suspendedUser', oauth2Token);
|
|
88
|
+
*
|
|
89
|
+
* console.log('Account suspended and all sessions terminated');
|
|
90
|
+
* } catch (error) {
|
|
91
|
+
* console.error('Account suspension failed:', error);
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* **Technical Implementation:**
|
|
96
|
+
* - Makes POST request to `https://identitytoolkit.googleapis.com/v1/accounts:update`
|
|
97
|
+
* - Sets `validSince` to current timestamp: `Math.floor(Date.now() / 1000)`
|
|
98
|
+
* - Uses `localId` parameter to identify the target user
|
|
99
|
+
* - Handles Firebase API error responses with detailed error information
|
|
100
|
+
* - Validates input parameters before making API calls
|
|
101
|
+
*
|
|
102
|
+
* **Testing Considerations:**
|
|
103
|
+
* - Can be verified by attempting to verify existing ID tokens with `checkRevoked=true`
|
|
104
|
+
* - Integration tests should create tokens, revoke them, then verify revocation
|
|
105
|
+
* - Unit tests should mock the fetch call and verify request format
|
|
106
|
+
* - Error scenarios should test various Firebase API error responses
|
|
107
|
+
*
|
|
108
|
+
* @see {@link verifyIdTokenHandler} For verifying ID tokens with revocation checking
|
|
109
|
+
* @see {@link updateUserHandler} For other user management operations
|
|
110
|
+
* @see {@link https://firebase.google.com/docs/auth/admin/manage-sessions#revoke_refresh_tokens Firebase Session Management}
|
|
111
|
+
* @see {@link https://firebase.google.com/docs/reference/rest/auth#section-update-account Firebase REST API}
|
|
112
|
+
*
|
|
113
|
+
* @package
|
|
114
|
+
* @since 1.0.0
|
|
115
|
+
*/
|
|
116
|
+
export async function revokeRefreshTokensHandler(uid, oauth2AccessToken) {
|
|
117
|
+
// Validate input parameters
|
|
118
|
+
if (typeof uid !== "string" || uid.length === 0) {
|
|
119
|
+
throw new Error("uid must be a non-empty string");
|
|
120
|
+
}
|
|
121
|
+
if (typeof oauth2AccessToken !== "string" || oauth2AccessToken.length === 0) {
|
|
122
|
+
throw new Error("oauth2AccessToken must be a non-empty string");
|
|
123
|
+
}
|
|
124
|
+
// Set validSince to current timestamp (in seconds)
|
|
125
|
+
const validSince = Math.floor(Date.now() / 1000).toString();
|
|
126
|
+
// Make the API call to revoke refresh tokens
|
|
127
|
+
const response = await fetch("https://identitytoolkit.googleapis.com/v1/accounts:update", {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: {
|
|
130
|
+
"Content-Type": "application/json",
|
|
131
|
+
Authorization: `Bearer ${oauth2AccessToken}`,
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
localId: uid,
|
|
135
|
+
validSince: validSince,
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
const errorText = await response.text();
|
|
140
|
+
let errorMessage;
|
|
141
|
+
try {
|
|
142
|
+
const errorData = JSON.parse(errorText);
|
|
143
|
+
const formattedErrorText = JSON.stringify(errorData, null, 2);
|
|
144
|
+
errorMessage = `Failed to revoke refresh tokens: ${response.status} ${response.statusText}\n${formattedErrorText}`;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
errorMessage = `Failed to revoke refresh tokens: ${response.status} ${response.statusText} - ${errorText}`;
|
|
148
|
+
}
|
|
149
|
+
throw new Error(errorMessage);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sets custom user claims for a Firebase Auth user.
|
|
3
|
+
*
|
|
4
|
+
* Custom claims are key-value pairs that provide additional information about a user
|
|
5
|
+
* that can be used for access control and authorization. These claims are included in
|
|
6
|
+
* the user's ID token and can be accessed by client applications.
|
|
7
|
+
*
|
|
8
|
+
* **Validation & Security:**
|
|
9
|
+
* - Validates claims against Firebase Auth and OIDC reserved names
|
|
10
|
+
* - Enforces 1000-byte size limit for serialized claims
|
|
11
|
+
* - Rejects invalid input types (arrays, primitives, etc.)
|
|
12
|
+
* - Prevents privilege escalation by blocking system claims
|
|
13
|
+
*
|
|
14
|
+
* **Firebase API Integration:**
|
|
15
|
+
* - Uses Firebase Admin API's `accounts:update` endpoint
|
|
16
|
+
* - Requires valid OAuth2 access token with admin privileges
|
|
17
|
+
* - Handles network errors and API response validation
|
|
18
|
+
* - Supports both setting new claims and clearing existing claims (null)
|
|
19
|
+
*
|
|
20
|
+
* **Common Use Cases:**
|
|
21
|
+
* - Role-based access control: `{ role: 'admin', department: 'IT' }`
|
|
22
|
+
* - Feature flags: `{ features: ['beta', 'premium'] }`
|
|
23
|
+
* - Organization membership: `{ orgId: 'acme-corp', permissions: ['read', 'write'] }`
|
|
24
|
+
* - Subscription levels: `{ plan: 'enterprise', expires: '2024-12-31' }`
|
|
25
|
+
*
|
|
26
|
+
* @param uid - The Firebase Auth user ID (localId) to set claims for.
|
|
27
|
+
* Must be a valid Firebase user identifier.
|
|
28
|
+
* @param customUserClaims - The custom claims object to set for the user.
|
|
29
|
+
* Pass `null` to clear all existing custom claims.
|
|
30
|
+
* Must comply with Firebase Auth restrictions.
|
|
31
|
+
* @param oauth2AccessToken - Valid OAuth2 access token with Firebase Admin API privileges.
|
|
32
|
+
* Obtained via service account authentication.
|
|
33
|
+
*
|
|
34
|
+
* @returns Promise that resolves when claims are successfully set.
|
|
35
|
+
*
|
|
36
|
+
* @throws {Error} When validation or API operations fail:
|
|
37
|
+
* - "Invalid custom user claims" - Claims validation failed (see checkClaimsAreValid)
|
|
38
|
+
* - "Failed to set custom user claims: {status} {statusText}, {body}" - Firebase API error
|
|
39
|
+
* - Network errors during Firebase API communication
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* // Set role-based claims
|
|
44
|
+
* await setCustomUserClaimsHandler(
|
|
45
|
+
* 'user123',
|
|
46
|
+
* { role: 'admin', department: 'engineering' },
|
|
47
|
+
* oauth2Token
|
|
48
|
+
* );
|
|
49
|
+
*
|
|
50
|
+
* // Set organization claims
|
|
51
|
+
* await setCustomUserClaimsHandler(
|
|
52
|
+
* 'user456',
|
|
53
|
+
* {
|
|
54
|
+
* organization: 'acme-corp',
|
|
55
|
+
* permissions: ['users:read', 'billing:write'],
|
|
56
|
+
* subscription: { plan: 'enterprise', expires: '2024-12-31' }
|
|
57
|
+
* },
|
|
58
|
+
* oauth2Token
|
|
59
|
+
* );
|
|
60
|
+
*
|
|
61
|
+
* // Clear all custom claims
|
|
62
|
+
* await setCustomUserClaimsHandler('user789', null, oauth2Token);
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* // Error handling
|
|
68
|
+
* try {
|
|
69
|
+
* await setCustomUserClaimsHandler(userId, claims, oauth2Token);
|
|
70
|
+
* console.log('Claims set successfully');
|
|
71
|
+
* } catch (error) {
|
|
72
|
+
* if (error.message.includes('Reserved claim name')) {
|
|
73
|
+
* console.error('Cannot use reserved claim names');
|
|
74
|
+
* } else if (error.message.includes('too large')) {
|
|
75
|
+
* console.error('Claims exceed 1000 byte limit');
|
|
76
|
+
* } else if (error.message.includes('Failed to set custom user claims')) {
|
|
77
|
+
* console.error('Firebase API error:', error.message);
|
|
78
|
+
* } else {
|
|
79
|
+
* console.error('Unexpected error:', error);
|
|
80
|
+
* }
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* **Important Notes:**
|
|
85
|
+
* - Claims are included in new ID tokens issued after this call
|
|
86
|
+
* - Existing ID tokens retain their original claims until refresh
|
|
87
|
+
* - Client applications can access claims via `firebase.auth().currentUser.getIdTokenResult()`
|
|
88
|
+
* - Claims persist until explicitly changed or user is deleted
|
|
89
|
+
* - Maximum 1000 custom claims per user (Firebase limit)
|
|
90
|
+
*
|
|
91
|
+
* @package
|
|
92
|
+
* @internal Used by CloudFireAuth.setCustomUserClaims()
|
|
93
|
+
* @since 1.0.0
|
|
94
|
+
*
|
|
95
|
+
* @see {@link https://firebase.google.com/docs/auth/admin/custom-claims Firebase Custom Claims Documentation}
|
|
96
|
+
* @see {@link https://firebase.google.com/docs/reference/admin/node/firebase-admin.auth.auth.md#authsetcustomuserclaims Firebase Admin SDK Reference}
|
|
97
|
+
* @see {@link checkClaimsAreValid} For validation rules and restrictions
|
|
98
|
+
*/
|
|
99
|
+
export declare function setCustomUserClaimsHandler(uid: string, customUserClaims: object | null, oauth2AccessToken: string): Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Validates custom user claims for Firebase Auth compliance.
|
|
102
|
+
*
|
|
103
|
+
* Performs comprehensive validation to ensure custom claims meet Firebase Auth requirements:
|
|
104
|
+
* - Rejects reserved Firebase Auth claim names (iss, aud, auth_time, user_id, firebase, etc.)
|
|
105
|
+
* - Rejects reserved OIDC standard claim names (email, name, picture, etc.)
|
|
106
|
+
* - Enforces the 1000-byte size limit for serialized claims
|
|
107
|
+
* - Handles null/undefined input gracefully
|
|
108
|
+
*
|
|
109
|
+
* Reserved Firebase Auth claims that cannot be overridden:
|
|
110
|
+
* - `iss`, `aud`, `auth_time`, `user_id`, `firebase`
|
|
111
|
+
* - `iat`, `exp`, `sub`, `uid`
|
|
112
|
+
*
|
|
113
|
+
* Reserved OIDC claims that cannot be overridden:
|
|
114
|
+
* - `email`, `email_verified`, `phone_number`, `phone_number_verified`
|
|
115
|
+
* - `name`, `given_name`, `family_name`, `middle_name`, `nickname`, `preferred_username`
|
|
116
|
+
* - `profile`, `picture`, `website`, `gender`, `birthdate`, `zoneinfo`, `locale`, `updated_at`
|
|
117
|
+
* - `azp`, `nonce`, `at_hash`, `c_hash`
|
|
118
|
+
*
|
|
119
|
+
* **Size Calculation:**
|
|
120
|
+
* Claims are serialized to JSON and must not exceed 1000 bytes when UTF-8 encoded.
|
|
121
|
+
* This includes all property names, values, and JSON formatting characters.
|
|
122
|
+
*
|
|
123
|
+
* @param customUserClaims - The custom user claims object to validate.
|
|
124
|
+
* Can be null to clear existing claims.
|
|
125
|
+
*
|
|
126
|
+
* @returns `true` if the claims are valid or null.
|
|
127
|
+
*
|
|
128
|
+
* @throws {Error} When claims violate Firebase Auth restrictions:
|
|
129
|
+
* - "Reserved claim name: {name} is not allowed in custom user claims" - Uses reserved Firebase/OIDC claim
|
|
130
|
+
* - "Custom user claims are too large. Must be less than 1000 bytes. Size: {size} bytes" - Exceeds size limit
|
|
131
|
+
* - "Failed to serialize custom user claims: {error}" - JSON serialization failed (circular refs, etc.)
|
|
132
|
+
* - Returns `false` for invalid input types (arrays, primitives, etc.)
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* // Valid claims
|
|
137
|
+
* checkClaimsAreValid({ role: 'admin', permissions: ['read', 'write'] }); // → true
|
|
138
|
+
* checkClaimsAreValid(null); // → true (clears claims)
|
|
139
|
+
* checkClaimsAreValid({}); // → true (empty claims)
|
|
140
|
+
*
|
|
141
|
+
* // Invalid input types return false
|
|
142
|
+
* checkClaimsAreValid([]); // → false (arrays not allowed)
|
|
143
|
+
* checkClaimsAreValid("string"); // → false (primitives not allowed)
|
|
144
|
+
*
|
|
145
|
+
* // Invalid claims throw errors
|
|
146
|
+
* try {
|
|
147
|
+
* checkClaimsAreValid({ email: 'test@example.com' });
|
|
148
|
+
* } catch (error) {
|
|
149
|
+
* // Error: Reserved claim name: email is not allowed in custom user claims
|
|
150
|
+
* }
|
|
151
|
+
*
|
|
152
|
+
* try {
|
|
153
|
+
* checkClaimsAreValid({ firebase: { tenant: 'abc' } });
|
|
154
|
+
* } catch (error) {
|
|
155
|
+
* // Error: Reserved claim name: firebase is not allowed in custom user claims
|
|
156
|
+
* }
|
|
157
|
+
*
|
|
158
|
+
* try {
|
|
159
|
+
* const largeClaims = { data: 'x'.repeat(1000) };
|
|
160
|
+
* checkClaimsAreValid(largeClaims);
|
|
161
|
+
* } catch (error) {
|
|
162
|
+
* // Error: Custom user claims are too large. Must be less than 1000 bytes. Size: 1012 bytes
|
|
163
|
+
* }
|
|
164
|
+
* ```
|
|
165
|
+
*
|
|
166
|
+
* @package
|
|
167
|
+
* @internal Used by setCustomUserClaims handler
|
|
168
|
+
* @since 1.0.0
|
|
169
|
+
*
|
|
170
|
+
* @see {@link https://firebase.google.com/docs/auth/admin/custom-claims Firebase Custom Claims}
|
|
171
|
+
* @see {@link https://openid.net/specs/openid-connect-core-1_0.html#IDToken OIDC ID Token Claims}
|
|
172
|
+
*/
|
|
173
|
+
export declare function checkClaimsAreValid(customUserClaims: object | null): boolean;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sets custom user claims for a Firebase Auth user.
|
|
3
|
+
*
|
|
4
|
+
* Custom claims are key-value pairs that provide additional information about a user
|
|
5
|
+
* that can be used for access control and authorization. These claims are included in
|
|
6
|
+
* the user's ID token and can be accessed by client applications.
|
|
7
|
+
*
|
|
8
|
+
* **Validation & Security:**
|
|
9
|
+
* - Validates claims against Firebase Auth and OIDC reserved names
|
|
10
|
+
* - Enforces 1000-byte size limit for serialized claims
|
|
11
|
+
* - Rejects invalid input types (arrays, primitives, etc.)
|
|
12
|
+
* - Prevents privilege escalation by blocking system claims
|
|
13
|
+
*
|
|
14
|
+
* **Firebase API Integration:**
|
|
15
|
+
* - Uses Firebase Admin API's `accounts:update` endpoint
|
|
16
|
+
* - Requires valid OAuth2 access token with admin privileges
|
|
17
|
+
* - Handles network errors and API response validation
|
|
18
|
+
* - Supports both setting new claims and clearing existing claims (null)
|
|
19
|
+
*
|
|
20
|
+
* **Common Use Cases:**
|
|
21
|
+
* - Role-based access control: `{ role: 'admin', department: 'IT' }`
|
|
22
|
+
* - Feature flags: `{ features: ['beta', 'premium'] }`
|
|
23
|
+
* - Organization membership: `{ orgId: 'acme-corp', permissions: ['read', 'write'] }`
|
|
24
|
+
* - Subscription levels: `{ plan: 'enterprise', expires: '2024-12-31' }`
|
|
25
|
+
*
|
|
26
|
+
* @param uid - The Firebase Auth user ID (localId) to set claims for.
|
|
27
|
+
* Must be a valid Firebase user identifier.
|
|
28
|
+
* @param customUserClaims - The custom claims object to set for the user.
|
|
29
|
+
* Pass `null` to clear all existing custom claims.
|
|
30
|
+
* Must comply with Firebase Auth restrictions.
|
|
31
|
+
* @param oauth2AccessToken - Valid OAuth2 access token with Firebase Admin API privileges.
|
|
32
|
+
* Obtained via service account authentication.
|
|
33
|
+
*
|
|
34
|
+
* @returns Promise that resolves when claims are successfully set.
|
|
35
|
+
*
|
|
36
|
+
* @throws {Error} When validation or API operations fail:
|
|
37
|
+
* - "Invalid custom user claims" - Claims validation failed (see checkClaimsAreValid)
|
|
38
|
+
* - "Failed to set custom user claims: {status} {statusText}, {body}" - Firebase API error
|
|
39
|
+
* - Network errors during Firebase API communication
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* // Set role-based claims
|
|
44
|
+
* await setCustomUserClaimsHandler(
|
|
45
|
+
* 'user123',
|
|
46
|
+
* { role: 'admin', department: 'engineering' },
|
|
47
|
+
* oauth2Token
|
|
48
|
+
* );
|
|
49
|
+
*
|
|
50
|
+
* // Set organization claims
|
|
51
|
+
* await setCustomUserClaimsHandler(
|
|
52
|
+
* 'user456',
|
|
53
|
+
* {
|
|
54
|
+
* organization: 'acme-corp',
|
|
55
|
+
* permissions: ['users:read', 'billing:write'],
|
|
56
|
+
* subscription: { plan: 'enterprise', expires: '2024-12-31' }
|
|
57
|
+
* },
|
|
58
|
+
* oauth2Token
|
|
59
|
+
* );
|
|
60
|
+
*
|
|
61
|
+
* // Clear all custom claims
|
|
62
|
+
* await setCustomUserClaimsHandler('user789', null, oauth2Token);
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* // Error handling
|
|
68
|
+
* try {
|
|
69
|
+
* await setCustomUserClaimsHandler(userId, claims, oauth2Token);
|
|
70
|
+
* console.log('Claims set successfully');
|
|
71
|
+
* } catch (error) {
|
|
72
|
+
* if (error.message.includes('Reserved claim name')) {
|
|
73
|
+
* console.error('Cannot use reserved claim names');
|
|
74
|
+
* } else if (error.message.includes('too large')) {
|
|
75
|
+
* console.error('Claims exceed 1000 byte limit');
|
|
76
|
+
* } else if (error.message.includes('Failed to set custom user claims')) {
|
|
77
|
+
* console.error('Firebase API error:', error.message);
|
|
78
|
+
* } else {
|
|
79
|
+
* console.error('Unexpected error:', error);
|
|
80
|
+
* }
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* **Important Notes:**
|
|
85
|
+
* - Claims are included in new ID tokens issued after this call
|
|
86
|
+
* - Existing ID tokens retain their original claims until refresh
|
|
87
|
+
* - Client applications can access claims via `firebase.auth().currentUser.getIdTokenResult()`
|
|
88
|
+
* - Claims persist until explicitly changed or user is deleted
|
|
89
|
+
* - Maximum 1000 custom claims per user (Firebase limit)
|
|
90
|
+
*
|
|
91
|
+
* @package
|
|
92
|
+
* @internal Used by CloudFireAuth.setCustomUserClaims()
|
|
93
|
+
* @since 1.0.0
|
|
94
|
+
*
|
|
95
|
+
* @see {@link https://firebase.google.com/docs/auth/admin/custom-claims Firebase Custom Claims Documentation}
|
|
96
|
+
* @see {@link https://firebase.google.com/docs/reference/admin/node/firebase-admin.auth.auth.md#authsetcustomuserclaims Firebase Admin SDK Reference}
|
|
97
|
+
* @see {@link checkClaimsAreValid} For validation rules and restrictions
|
|
98
|
+
*/
|
|
99
|
+
export async function setCustomUserClaimsHandler(uid, customUserClaims, oauth2AccessToken) {
|
|
100
|
+
if (!checkClaimsAreValid(customUserClaims)) {
|
|
101
|
+
throw new Error("Invalid custom user claims", { cause: customUserClaims });
|
|
102
|
+
}
|
|
103
|
+
const body = JSON.stringify({
|
|
104
|
+
localId: uid,
|
|
105
|
+
customAttributes: JSON.stringify(customUserClaims ?? {}),
|
|
106
|
+
});
|
|
107
|
+
console.log("body", body);
|
|
108
|
+
const response = await fetch(`https://identitytoolkit.googleapis.com/v1/accounts:update`, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: {
|
|
111
|
+
"Content-Type": "application/json",
|
|
112
|
+
Authorization: `Bearer ${oauth2AccessToken}`,
|
|
113
|
+
},
|
|
114
|
+
body,
|
|
115
|
+
});
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
console.log("response", response);
|
|
118
|
+
throw new Error(`Failed to set custom user claims: ${response.status} ${response.statusText}, ${await response.text()}, response: ${response}`);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Validates custom user claims for Firebase Auth compliance.
|
|
124
|
+
*
|
|
125
|
+
* Performs comprehensive validation to ensure custom claims meet Firebase Auth requirements:
|
|
126
|
+
* - Rejects reserved Firebase Auth claim names (iss, aud, auth_time, user_id, firebase, etc.)
|
|
127
|
+
* - Rejects reserved OIDC standard claim names (email, name, picture, etc.)
|
|
128
|
+
* - Enforces the 1000-byte size limit for serialized claims
|
|
129
|
+
* - Handles null/undefined input gracefully
|
|
130
|
+
*
|
|
131
|
+
* Reserved Firebase Auth claims that cannot be overridden:
|
|
132
|
+
* - `iss`, `aud`, `auth_time`, `user_id`, `firebase`
|
|
133
|
+
* - `iat`, `exp`, `sub`, `uid`
|
|
134
|
+
*
|
|
135
|
+
* Reserved OIDC claims that cannot be overridden:
|
|
136
|
+
* - `email`, `email_verified`, `phone_number`, `phone_number_verified`
|
|
137
|
+
* - `name`, `given_name`, `family_name`, `middle_name`, `nickname`, `preferred_username`
|
|
138
|
+
* - `profile`, `picture`, `website`, `gender`, `birthdate`, `zoneinfo`, `locale`, `updated_at`
|
|
139
|
+
* - `azp`, `nonce`, `at_hash`, `c_hash`
|
|
140
|
+
*
|
|
141
|
+
* **Size Calculation:**
|
|
142
|
+
* Claims are serialized to JSON and must not exceed 1000 bytes when UTF-8 encoded.
|
|
143
|
+
* This includes all property names, values, and JSON formatting characters.
|
|
144
|
+
*
|
|
145
|
+
* @param customUserClaims - The custom user claims object to validate.
|
|
146
|
+
* Can be null to clear existing claims.
|
|
147
|
+
*
|
|
148
|
+
* @returns `true` if the claims are valid or null.
|
|
149
|
+
*
|
|
150
|
+
* @throws {Error} When claims violate Firebase Auth restrictions:
|
|
151
|
+
* - "Reserved claim name: {name} is not allowed in custom user claims" - Uses reserved Firebase/OIDC claim
|
|
152
|
+
* - "Custom user claims are too large. Must be less than 1000 bytes. Size: {size} bytes" - Exceeds size limit
|
|
153
|
+
* - "Failed to serialize custom user claims: {error}" - JSON serialization failed (circular refs, etc.)
|
|
154
|
+
* - Returns `false` for invalid input types (arrays, primitives, etc.)
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```typescript
|
|
158
|
+
* // Valid claims
|
|
159
|
+
* checkClaimsAreValid({ role: 'admin', permissions: ['read', 'write'] }); // → true
|
|
160
|
+
* checkClaimsAreValid(null); // → true (clears claims)
|
|
161
|
+
* checkClaimsAreValid({}); // → true (empty claims)
|
|
162
|
+
*
|
|
163
|
+
* // Invalid input types return false
|
|
164
|
+
* checkClaimsAreValid([]); // → false (arrays not allowed)
|
|
165
|
+
* checkClaimsAreValid("string"); // → false (primitives not allowed)
|
|
166
|
+
*
|
|
167
|
+
* // Invalid claims throw errors
|
|
168
|
+
* try {
|
|
169
|
+
* checkClaimsAreValid({ email: 'test@example.com' });
|
|
170
|
+
* } catch (error) {
|
|
171
|
+
* // Error: Reserved claim name: email is not allowed in custom user claims
|
|
172
|
+
* }
|
|
173
|
+
*
|
|
174
|
+
* try {
|
|
175
|
+
* checkClaimsAreValid({ firebase: { tenant: 'abc' } });
|
|
176
|
+
* } catch (error) {
|
|
177
|
+
* // Error: Reserved claim name: firebase is not allowed in custom user claims
|
|
178
|
+
* }
|
|
179
|
+
*
|
|
180
|
+
* try {
|
|
181
|
+
* const largeClaims = { data: 'x'.repeat(1000) };
|
|
182
|
+
* checkClaimsAreValid(largeClaims);
|
|
183
|
+
* } catch (error) {
|
|
184
|
+
* // Error: Custom user claims are too large. Must be less than 1000 bytes. Size: 1012 bytes
|
|
185
|
+
* }
|
|
186
|
+
* ```
|
|
187
|
+
*
|
|
188
|
+
* @package
|
|
189
|
+
* @internal Used by setCustomUserClaims handler
|
|
190
|
+
* @since 1.0.0
|
|
191
|
+
*
|
|
192
|
+
* @see {@link https://firebase.google.com/docs/auth/admin/custom-claims Firebase Custom Claims}
|
|
193
|
+
* @see {@link https://openid.net/specs/openid-connect-core-1_0.html#IDToken OIDC ID Token Claims}
|
|
194
|
+
*/
|
|
195
|
+
export function checkClaimsAreValid(customUserClaims) {
|
|
196
|
+
// Null is valid (used to clear existing claims)
|
|
197
|
+
if (customUserClaims === null || customUserClaims === undefined) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
// Must be an object
|
|
201
|
+
if (typeof customUserClaims !== "object" || Array.isArray(customUserClaims)) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
const claims = customUserClaims;
|
|
205
|
+
// Reserved Firebase Auth claims
|
|
206
|
+
const firebaseReservedClaims = new Set([
|
|
207
|
+
"iss",
|
|
208
|
+
"aud",
|
|
209
|
+
"auth_time",
|
|
210
|
+
"user_id",
|
|
211
|
+
"firebase",
|
|
212
|
+
"iat",
|
|
213
|
+
"exp",
|
|
214
|
+
"sub",
|
|
215
|
+
"uid",
|
|
216
|
+
]);
|
|
217
|
+
// Reserved OIDC standard claims
|
|
218
|
+
const oidcReservedClaims = new Set([
|
|
219
|
+
"email",
|
|
220
|
+
"email_verified",
|
|
221
|
+
"phone_number",
|
|
222
|
+
"phone_number_verified",
|
|
223
|
+
"name",
|
|
224
|
+
"given_name",
|
|
225
|
+
"family_name",
|
|
226
|
+
"middle_name",
|
|
227
|
+
"nickname",
|
|
228
|
+
"preferred_username",
|
|
229
|
+
"profile",
|
|
230
|
+
"picture",
|
|
231
|
+
"website",
|
|
232
|
+
"gender",
|
|
233
|
+
"birthdate",
|
|
234
|
+
"zoneinfo",
|
|
235
|
+
"locale",
|
|
236
|
+
"updated_at",
|
|
237
|
+
"azp",
|
|
238
|
+
"nonce",
|
|
239
|
+
"at_hash",
|
|
240
|
+
"c_hash",
|
|
241
|
+
]);
|
|
242
|
+
// Check for reserved claim names
|
|
243
|
+
for (const key of Object.keys(claims)) {
|
|
244
|
+
if (firebaseReservedClaims.has(key) || oidcReservedClaims.has(key)) {
|
|
245
|
+
throw new Error(`Reserved claim name: ${key} is not allowed in custom user claims`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Check size limit (1000 bytes when JSON serialized)
|
|
249
|
+
try {
|
|
250
|
+
const serialized = JSON.stringify(claims);
|
|
251
|
+
const sizeInBytes = new TextEncoder().encode(serialized).length;
|
|
252
|
+
if (sizeInBytes > 1000) {
|
|
253
|
+
throw new Error(`Custom user claims are too large. Must be less than 1000 bytes. Size: ${sizeInBytes} bytes`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
// JSON serialization failed (circular references, etc.)
|
|
258
|
+
throw new Error(`Failed to serialize custom user claims: ${error}`);
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes a standard request to the Firebase Auth REST API. AS all the
|
|
3
|
+
* requests use the same authentication method, this function is used to
|
|
4
|
+
* make the request.
|
|
5
|
+
*
|
|
6
|
+
* @param url - The URL to make the request to.
|
|
7
|
+
* @param method - The HTTP method to use.
|
|
8
|
+
* @param oauth2Token - The OAuth2 token to use for authentication.
|
|
9
|
+
* @param body - The body of the request.
|
|
10
|
+
* @returns The response from the request.
|
|
11
|
+
*/
|
|
12
|
+
export declare function standardRequest(url: string, method: string, oauth2Token: string, body?: any): Promise<Response>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes a standard request to the Firebase Auth REST API. AS all the
|
|
3
|
+
* requests use the same authentication method, this function is used to
|
|
4
|
+
* make the request.
|
|
5
|
+
*
|
|
6
|
+
* @param url - The URL to make the request to.
|
|
7
|
+
* @param method - The HTTP method to use.
|
|
8
|
+
* @param oauth2Token - The OAuth2 token to use for authentication.
|
|
9
|
+
* @param body - The body of the request.
|
|
10
|
+
* @returns The response from the request.
|
|
11
|
+
*/
|
|
12
|
+
export async function standardRequest(url, method, oauth2Token, body) {
|
|
13
|
+
return fetch(url, {
|
|
14
|
+
method,
|
|
15
|
+
body,
|
|
16
|
+
headers: {
|
|
17
|
+
Authorization: `Bearer ${oauth2Token}`,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|