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,273 @@
|
|
|
1
|
+
import { decodeJwt, decodeProtectedHeader, importX509, jwtVerify } from "jose";
|
|
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 async function verifyIdTokenHandler(idToken, projectId, oauth2Token, kv, checkRevoked) {
|
|
110
|
+
const { isValid: isValidBody, errorMessage: errorMessageBody } = await validateJwtBody(idToken, projectId);
|
|
111
|
+
if (!isValidBody) {
|
|
112
|
+
throw new Error(errorMessageBody);
|
|
113
|
+
}
|
|
114
|
+
const { isValid: isValidHeader, errorMessage: errorMessageHeader, signingKey } = await validateJwtHeader(idToken, kv);
|
|
115
|
+
if (!isValidHeader) {
|
|
116
|
+
throw new Error(errorMessageHeader);
|
|
117
|
+
}
|
|
118
|
+
const { isValid: tokenVerified, payload: tokenPayload } = await verifyToken(idToken, signingKey, projectId);
|
|
119
|
+
if (!tokenVerified) {
|
|
120
|
+
throw new Error("Token is invalid");
|
|
121
|
+
}
|
|
122
|
+
const localId = tokenPayload?.sub;
|
|
123
|
+
if (checkRevoked === true) {
|
|
124
|
+
const tokenValidSinceTime = await getTokenValidSinceTime(localId, oauth2Token);
|
|
125
|
+
if (tokenPayload?.iat && tokenValidSinceTime > tokenPayload.iat) {
|
|
126
|
+
throw new Error("Token is revoked");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return tokenPayload;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Gets the token validSince time for the user corresponding to the ID token.
|
|
133
|
+
* If the token was issued before this validSince time, the token is invalid.
|
|
134
|
+
* @param localId - The local ID of the user to get the token valid since time for.
|
|
135
|
+
* @param oauth2Token - The OAuth2 token for the Firebase Admin API.
|
|
136
|
+
* @returns The token valid since time.
|
|
137
|
+
*/
|
|
138
|
+
async function getTokenValidSinceTime(localId, oauth2Token) {
|
|
139
|
+
const accountLookupResponse = await fetch(`https://identitytoolkit.googleapis.com/v1/accounts:lookup`, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: {
|
|
142
|
+
"Content-Type": "application/json",
|
|
143
|
+
Authorization: `Bearer ${oauth2Token}`,
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify({ localId: [localId] }),
|
|
146
|
+
});
|
|
147
|
+
const accountLookupJson = (await accountLookupResponse.json());
|
|
148
|
+
if (!accountLookupJson.users[0]?.validSince) {
|
|
149
|
+
throw new Error("Token valid since time is not a string");
|
|
150
|
+
}
|
|
151
|
+
return parseInt(accountLookupJson.users[0].validSince);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Validates the body of a JWT. The token is expected to come from Firebase Auth,
|
|
155
|
+
* because of this it has specific required fields that are checked for. If these
|
|
156
|
+
* fields are not present, the token is invalid.
|
|
157
|
+
*
|
|
158
|
+
* @param token - The JWT to validate.
|
|
159
|
+
* @returns True if the token is valid, false and an error message if the token is invalid.
|
|
160
|
+
*/
|
|
161
|
+
async function validateJwtBody(token, projectId) {
|
|
162
|
+
const firebaseJwtBody = decodeJwt(token);
|
|
163
|
+
if (firebaseJwtBody.aud !== projectId) {
|
|
164
|
+
return {
|
|
165
|
+
isValid: false,
|
|
166
|
+
errorMessage: `Token audience does not match project ID, expected ${projectId}, got ${firebaseJwtBody.aud}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (typeof firebaseJwtBody.sub !== "string") {
|
|
170
|
+
return {
|
|
171
|
+
isValid: false,
|
|
172
|
+
errorMessage: "Token subject is not a string",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (firebaseJwtBody.sub === "") {
|
|
176
|
+
return {
|
|
177
|
+
isValid: false,
|
|
178
|
+
errorMessage: `Token subject is empty`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (firebaseJwtBody.iss !== `https://securetoken.google.com/${projectId}`) {
|
|
182
|
+
return {
|
|
183
|
+
isValid: false,
|
|
184
|
+
errorMessage: `Token issuer does not match project ID, expected https://securetoken.google.com/${projectId}, got ${firebaseJwtBody.iss}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const currentTime = Math.ceil(Date.now() / 1000);
|
|
188
|
+
if (firebaseJwtBody.exp && firebaseJwtBody.exp < currentTime) {
|
|
189
|
+
return { isValid: false, errorMessage: "Token expiration date is in the past" };
|
|
190
|
+
}
|
|
191
|
+
if (firebaseJwtBody.iat && firebaseJwtBody.iat > currentTime) {
|
|
192
|
+
return { isValid: false, errorMessage: "Token issued at date is in the future" };
|
|
193
|
+
}
|
|
194
|
+
if (firebaseJwtBody.auth_time && typeof firebaseJwtBody.auth_time !== "number") {
|
|
195
|
+
return { isValid: false, errorMessage: "Token auth time is not a number" };
|
|
196
|
+
}
|
|
197
|
+
if (firebaseJwtBody.auth_time > currentTime) {
|
|
198
|
+
return { isValid: false, errorMessage: "Token auth time is in the future" };
|
|
199
|
+
}
|
|
200
|
+
return { isValid: true };
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Validates the header of a Firebase JWT. First checks the algorithm type is
|
|
204
|
+
* the expected type, then checks the key ID is valid either by checking in the
|
|
205
|
+
* KV namespace or by fetching the key from the Google API.
|
|
206
|
+
*
|
|
207
|
+
* If a new key is fetched from the Google API that was used to sign the token
|
|
208
|
+
* then it is stored in the KV namespace for the duration of the cache time
|
|
209
|
+
* returned from the Google API in the headers.
|
|
210
|
+
*
|
|
211
|
+
* @param token - The JWT to validate.
|
|
212
|
+
* @param kv - The KV namespace to get the Google public keys from.
|
|
213
|
+
* @returns True and the signing key if the token is valid, false and an error message if the token is invalid.
|
|
214
|
+
*/
|
|
215
|
+
export async function validateJwtHeader(token, kv) {
|
|
216
|
+
const firebaseJwtHeader = decodeProtectedHeader(token);
|
|
217
|
+
// console.log("firebaseJwtHeader", firebaseJwtHeader);
|
|
218
|
+
if (firebaseJwtHeader.alg !== "RS256") {
|
|
219
|
+
return { isValid: false, errorMessage: "Token algorithm is not RS256" };
|
|
220
|
+
}
|
|
221
|
+
const expectedKeyId = `googlePublicKey-${firebaseJwtHeader.kid}`;
|
|
222
|
+
if (kv) {
|
|
223
|
+
const googlePublicKey = await kv.get(expectedKeyId);
|
|
224
|
+
if (googlePublicKey) {
|
|
225
|
+
return { isValid: true, signingKey: googlePublicKey };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
console.log("Token key ID is not in the KV namespace, fetching from Google API");
|
|
229
|
+
const googlePublicKeys = await getGooglePublicKeys();
|
|
230
|
+
const signingKey = googlePublicKeys.keys[firebaseJwtHeader.kid];
|
|
231
|
+
if (!signingKey) {
|
|
232
|
+
return { isValid: false, errorMessage: "Token key ID is not in the Google API" };
|
|
233
|
+
}
|
|
234
|
+
if (kv) {
|
|
235
|
+
const cacheDuration = googlePublicKeys.cache_duration;
|
|
236
|
+
if (cacheDuration > 0) {
|
|
237
|
+
await kv.put(expectedKeyId, signingKey, { expirationTtl: cacheDuration });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return { isValid: true, signingKey };
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Pulls the google public keys from the Google API and extracts the cache time
|
|
244
|
+
* for the keys. Returns these in an object.
|
|
245
|
+
* @returns The Google public keys.
|
|
246
|
+
*/
|
|
247
|
+
async function getGooglePublicKeys() {
|
|
248
|
+
const googlePublicKeysResponse = await fetch("https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com");
|
|
249
|
+
console.log("googlePublicKeysResponse", googlePublicKeysResponse.status);
|
|
250
|
+
const googlePublicKeysJson = await googlePublicKeysResponse.json();
|
|
251
|
+
const cacheDuration = googlePublicKeysResponse.headers.get("cache-control")?.split("=")[1];
|
|
252
|
+
const googlePublicKeys = {
|
|
253
|
+
keys: googlePublicKeysJson,
|
|
254
|
+
cache_duration: cacheDuration ? parseInt(cacheDuration) : 0,
|
|
255
|
+
};
|
|
256
|
+
return googlePublicKeys;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Verifies a Firebase ID token using Google' public keys, and their suggested
|
|
260
|
+
* simple validation checks. You can read about these checks here:
|
|
261
|
+
* https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_a_third-party_jwt_library
|
|
262
|
+
* @param token - The Firebase ID token to verify
|
|
263
|
+
* @returns True if the token is valid, false otherwise
|
|
264
|
+
*/
|
|
265
|
+
async function verifyToken(token, signingKey, projectId) {
|
|
266
|
+
const tokenVerificationKey = await importX509(signingKey, "RS256");
|
|
267
|
+
const { payload } = await jwtVerify(token, tokenVerificationKey, {
|
|
268
|
+
issuer: `https://securetoken.google.com/${projectId}`,
|
|
269
|
+
audience: projectId,
|
|
270
|
+
});
|
|
271
|
+
console.log("Token verified");
|
|
272
|
+
return { isValid: true, payload: payload };
|
|
273
|
+
}
|