@prmichaelsen/firebase-admin-sdk-v8 2.3.1 → 2.4.2
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/AGENT.md +117 -6
- package/CHANGELOG.md +14 -0
- package/README.md +21 -1
- package/dist/chunk-5X465GLA.mjs +161 -0
- package/dist/index.d.mts +55 -1
- package/dist/index.d.ts +55 -1
- package/dist/index.js +330 -125
- package/dist/index.mjs +178 -162
- package/dist/token-generation-5K7K6T6U.mjs +8 -0
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,81 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
function getServiceAccount() {
|
|
13
|
-
if (globalConfig.serviceAccount) {
|
|
14
|
-
if (typeof globalConfig.serviceAccount === "string") {
|
|
15
|
-
return JSON.parse(globalConfig.serviceAccount);
|
|
16
|
-
}
|
|
17
|
-
return globalConfig.serviceAccount;
|
|
18
|
-
}
|
|
19
|
-
const key = typeof process !== "undefined" && process.env?.FIREBASE_ADMIN_SERVICE_ACCOUNT_KEY;
|
|
20
|
-
if (!key) {
|
|
21
|
-
throw new Error(
|
|
22
|
-
"Firebase service account not configured. Either call initializeApp({ serviceAccount: ... }) or set FIREBASE_ADMIN_SERVICE_ACCOUNT_KEY environment variable."
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
try {
|
|
26
|
-
const serviceAccount = JSON.parse(key);
|
|
27
|
-
const requiredFields = [
|
|
28
|
-
"type",
|
|
29
|
-
"project_id",
|
|
30
|
-
"private_key_id",
|
|
31
|
-
"private_key",
|
|
32
|
-
"client_email",
|
|
33
|
-
"client_id",
|
|
34
|
-
"token_uri"
|
|
35
|
-
];
|
|
36
|
-
for (const field of requiredFields) {
|
|
37
|
-
if (!(field in serviceAccount)) {
|
|
38
|
-
throw new Error(`Service account is missing required field: ${field}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return serviceAccount;
|
|
42
|
-
} catch (error) {
|
|
43
|
-
if (error instanceof SyntaxError) {
|
|
44
|
-
throw new Error(
|
|
45
|
-
"Failed to parse FIREBASE_ADMIN_SERVICE_ACCOUNT_KEY. Ensure it contains valid JSON."
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
throw error;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function getProjectId() {
|
|
52
|
-
if (globalConfig.projectId) {
|
|
53
|
-
return globalConfig.projectId;
|
|
54
|
-
}
|
|
55
|
-
if (typeof process !== "undefined" && process.env) {
|
|
56
|
-
const projectId = process.env.FIREBASE_PROJECT_ID || process.env.PUBLIC_FIREBASE_PROJECT_ID;
|
|
57
|
-
if (projectId) {
|
|
58
|
-
return projectId;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
throw new Error(
|
|
62
|
-
"Firebase project ID not configured. Either call initializeApp({ projectId: ... }) or set FIREBASE_PROJECT_ID environment variable."
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
function getFirebaseApiKey() {
|
|
66
|
-
if (globalConfig.apiKey) {
|
|
67
|
-
return globalConfig.apiKey;
|
|
68
|
-
}
|
|
69
|
-
if (typeof process !== "undefined" && process.env) {
|
|
70
|
-
const apiKey = process.env.FIREBASE_API_KEY || process.env.PUBLIC_FIREBASE_API_KEY;
|
|
71
|
-
if (apiKey) {
|
|
72
|
-
return apiKey;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
throw new Error(
|
|
76
|
-
"Firebase API key not configured. Either call initializeApp({ apiKey: ... }) or set FIREBASE_API_KEY environment variable. Find your API key in Firebase Console > Project Settings > Web API Key."
|
|
77
|
-
);
|
|
78
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
clearConfig,
|
|
3
|
+
clearTokenCache,
|
|
4
|
+
getAdminAccessToken,
|
|
5
|
+
getConfig,
|
|
6
|
+
getFirebaseApiKey,
|
|
7
|
+
getProjectId,
|
|
8
|
+
getServiceAccount,
|
|
9
|
+
initializeApp
|
|
10
|
+
} from "./chunk-5X465GLA.mjs";
|
|
79
11
|
|
|
80
12
|
// src/x509.ts
|
|
81
13
|
function decodeBase64(str) {
|
|
@@ -166,23 +98,41 @@ async function importPublicKeyFromX509(pem) {
|
|
|
166
98
|
}
|
|
167
99
|
|
|
168
100
|
// src/auth.ts
|
|
169
|
-
var
|
|
170
|
-
var
|
|
101
|
+
var idTokenKeysCache = null;
|
|
102
|
+
var idTokenKeysCacheExpiry = 0;
|
|
103
|
+
var sessionKeysCache = null;
|
|
104
|
+
var sessionKeysCacheExpiry = 0;
|
|
171
105
|
async function fetchPublicKeys(issuer) {
|
|
172
|
-
|
|
173
|
-
|
|
106
|
+
const isSessionCookie = issuer && issuer.includes("session.firebase.google.com");
|
|
107
|
+
let endpoint;
|
|
108
|
+
let cache;
|
|
109
|
+
let cacheExpiry;
|
|
110
|
+
if (isSessionCookie) {
|
|
174
111
|
endpoint = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys";
|
|
112
|
+
cache = sessionKeysCache;
|
|
113
|
+
cacheExpiry = sessionKeysCacheExpiry;
|
|
114
|
+
} else {
|
|
115
|
+
endpoint = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com";
|
|
116
|
+
cache = idTokenKeysCache;
|
|
117
|
+
cacheExpiry = idTokenKeysCacheExpiry;
|
|
175
118
|
}
|
|
176
|
-
if (
|
|
177
|
-
return
|
|
119
|
+
if (cache && Date.now() < cacheExpiry) {
|
|
120
|
+
return cache;
|
|
178
121
|
}
|
|
179
122
|
const response = await fetch(endpoint);
|
|
180
123
|
if (!response.ok) {
|
|
181
124
|
throw new Error(`Failed to fetch Firebase public keys from ${endpoint}`);
|
|
182
125
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
126
|
+
const keys = await response.json();
|
|
127
|
+
const newExpiry = Date.now() + 36e5;
|
|
128
|
+
if (isSessionCookie) {
|
|
129
|
+
sessionKeysCache = keys;
|
|
130
|
+
sessionKeysCacheExpiry = newExpiry;
|
|
131
|
+
} else {
|
|
132
|
+
idTokenKeysCache = keys;
|
|
133
|
+
idTokenKeysCacheExpiry = newExpiry;
|
|
134
|
+
}
|
|
135
|
+
return keys;
|
|
186
136
|
}
|
|
187
137
|
function base64UrlDecode(str) {
|
|
188
138
|
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -257,8 +207,14 @@ async function verifyIdToken(idToken) {
|
|
|
257
207
|
let publicKeys = await fetchPublicKeys(payload.iss);
|
|
258
208
|
let publicKeyPem = publicKeys[header.kid];
|
|
259
209
|
if (!publicKeyPem) {
|
|
260
|
-
|
|
261
|
-
|
|
210
|
+
const isSessionCookie = payload.iss && payload.iss.includes("session.firebase.google.com");
|
|
211
|
+
if (isSessionCookie) {
|
|
212
|
+
sessionKeysCache = null;
|
|
213
|
+
sessionKeysCacheExpiry = 0;
|
|
214
|
+
} else {
|
|
215
|
+
idTokenKeysCache = null;
|
|
216
|
+
idTokenKeysCacheExpiry = 0;
|
|
217
|
+
}
|
|
262
218
|
publicKeys = await fetchPublicKeys(payload.iss);
|
|
263
219
|
publicKeyPem = publicKeys[header.kid];
|
|
264
220
|
if (!publicKeyPem) {
|
|
@@ -392,6 +348,136 @@ async function signInWithCustomToken(customToken) {
|
|
|
392
348
|
isNewUser: result.isNewUser
|
|
393
349
|
};
|
|
394
350
|
}
|
|
351
|
+
async function createSessionCookie(idToken, options) {
|
|
352
|
+
if (!idToken || typeof idToken !== "string") {
|
|
353
|
+
throw new Error("idToken must be a non-empty string");
|
|
354
|
+
}
|
|
355
|
+
if (!options.expiresIn || typeof options.expiresIn !== "number") {
|
|
356
|
+
throw new Error("expiresIn must be a number");
|
|
357
|
+
}
|
|
358
|
+
const MIN_DURATION = 5 * 60 * 1e3;
|
|
359
|
+
const MAX_DURATION = 14 * 24 * 60 * 60 * 1e3;
|
|
360
|
+
if (options.expiresIn < MIN_DURATION) {
|
|
361
|
+
throw new Error(`expiresIn must be at least ${MIN_DURATION}ms (5 minutes)`);
|
|
362
|
+
}
|
|
363
|
+
if (options.expiresIn > MAX_DURATION) {
|
|
364
|
+
throw new Error(`expiresIn must be at most ${MAX_DURATION}ms (14 days)`);
|
|
365
|
+
}
|
|
366
|
+
const { getAdminAccessToken: getAdminAccessToken2 } = await import("./token-generation-5K7K6T6U.mjs");
|
|
367
|
+
const accessToken = await getAdminAccessToken2();
|
|
368
|
+
const projectId = getProjectId();
|
|
369
|
+
const url = `https://identitytoolkit.googleapis.com/v1/projects/${projectId}:createSessionCookie`;
|
|
370
|
+
const validDurationSeconds = Math.floor(options.expiresIn / 1e3);
|
|
371
|
+
const response = await fetch(url, {
|
|
372
|
+
method: "POST",
|
|
373
|
+
headers: {
|
|
374
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
375
|
+
"Content-Type": "application/json"
|
|
376
|
+
},
|
|
377
|
+
body: JSON.stringify({
|
|
378
|
+
idToken,
|
|
379
|
+
validDuration: validDurationSeconds.toString()
|
|
380
|
+
// Must be string per API spec
|
|
381
|
+
})
|
|
382
|
+
});
|
|
383
|
+
if (!response.ok) {
|
|
384
|
+
const errorText = await response.text();
|
|
385
|
+
let errorMessage = `Failed to create session cookie: ${response.status}`;
|
|
386
|
+
try {
|
|
387
|
+
const errorJson = JSON.parse(errorText);
|
|
388
|
+
if (errorJson.error && errorJson.error.message) {
|
|
389
|
+
errorMessage += ` - ${errorJson.error.message}`;
|
|
390
|
+
}
|
|
391
|
+
} catch {
|
|
392
|
+
errorMessage += ` - ${errorText}`;
|
|
393
|
+
}
|
|
394
|
+
throw new Error(errorMessage);
|
|
395
|
+
}
|
|
396
|
+
const result = await response.json();
|
|
397
|
+
return result.sessionCookie;
|
|
398
|
+
}
|
|
399
|
+
async function verifySessionCookie(sessionCookie, checkRevoked = false) {
|
|
400
|
+
if (!sessionCookie || typeof sessionCookie !== "string") {
|
|
401
|
+
throw new Error("sessionCookie must be a non-empty string");
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
const parts = sessionCookie.split(".");
|
|
405
|
+
if (parts.length !== 3) {
|
|
406
|
+
throw new Error("Invalid session cookie format");
|
|
407
|
+
}
|
|
408
|
+
const [headerB64, payloadB64] = parts;
|
|
409
|
+
const headerJson = atob(headerB64.replace(/-/g, "+").replace(/_/g, "/"));
|
|
410
|
+
const header = JSON.parse(headerJson);
|
|
411
|
+
const payloadJson = atob(payloadB64.replace(/-/g, "+").replace(/_/g, "/"));
|
|
412
|
+
const payload = JSON.parse(payloadJson);
|
|
413
|
+
const projectId = getProjectId();
|
|
414
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
415
|
+
if (!payload.exp || payload.exp < now) {
|
|
416
|
+
throw new Error("Session cookie has expired");
|
|
417
|
+
}
|
|
418
|
+
if (!payload.iat || payload.iat > now) {
|
|
419
|
+
throw new Error("Session cookie issued in the future");
|
|
420
|
+
}
|
|
421
|
+
if (payload.aud !== projectId) {
|
|
422
|
+
throw new Error(`Session cookie has incorrect audience. Expected ${projectId}, got ${payload.aud}`);
|
|
423
|
+
}
|
|
424
|
+
const expectedIssuer = `https://session.firebase.google.com/${projectId}`;
|
|
425
|
+
if (payload.iss !== expectedIssuer) {
|
|
426
|
+
throw new Error(`Session cookie has incorrect issuer. Expected ${expectedIssuer}, got ${payload.iss}`);
|
|
427
|
+
}
|
|
428
|
+
if (!payload.sub || typeof payload.sub !== "string" || payload.sub.length === 0) {
|
|
429
|
+
throw new Error("Session cookie has no subject (user ID)");
|
|
430
|
+
}
|
|
431
|
+
await verifySessionCookieSignature(sessionCookie, header, payload);
|
|
432
|
+
if (checkRevoked) {
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
uid: payload.sub,
|
|
436
|
+
aud: payload.aud,
|
|
437
|
+
auth_time: payload.auth_time,
|
|
438
|
+
exp: payload.exp,
|
|
439
|
+
iat: payload.iat,
|
|
440
|
+
iss: payload.iss,
|
|
441
|
+
sub: payload.sub,
|
|
442
|
+
email: payload.email,
|
|
443
|
+
email_verified: payload.email_verified,
|
|
444
|
+
firebase: payload.firebase,
|
|
445
|
+
...payload
|
|
446
|
+
};
|
|
447
|
+
} catch (error) {
|
|
448
|
+
throw new Error(`Failed to verify session cookie: ${error.message}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async function verifySessionCookieSignature(jwt, header, payload) {
|
|
452
|
+
const keys = await fetchPublicKeys(payload.iss);
|
|
453
|
+
const kid = header.kid;
|
|
454
|
+
if (!kid || !keys[kid]) {
|
|
455
|
+
throw new Error("Session cookie has invalid key ID");
|
|
456
|
+
}
|
|
457
|
+
const publicKeyPem = keys[kid];
|
|
458
|
+
const publicKey = await importPublicKeyFromX509(publicKeyPem);
|
|
459
|
+
const [headerAndPayload, signature] = [
|
|
460
|
+
jwt.split(".").slice(0, 2).join("."),
|
|
461
|
+
jwt.split(".")[2]
|
|
462
|
+
];
|
|
463
|
+
const encoder = new TextEncoder();
|
|
464
|
+
const data = encoder.encode(headerAndPayload);
|
|
465
|
+
const signatureBase64 = signature.replace(/-/g, "+").replace(/_/g, "/");
|
|
466
|
+
const signatureBinary = atob(signatureBase64);
|
|
467
|
+
const signatureBytes = new Uint8Array(signatureBinary.length);
|
|
468
|
+
for (let i = 0; i < signatureBinary.length; i++) {
|
|
469
|
+
signatureBytes[i] = signatureBinary.charCodeAt(i);
|
|
470
|
+
}
|
|
471
|
+
const isValid = await crypto.subtle.verify(
|
|
472
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
473
|
+
publicKey,
|
|
474
|
+
signatureBytes,
|
|
475
|
+
data
|
|
476
|
+
);
|
|
477
|
+
if (!isValid) {
|
|
478
|
+
throw new Error("Session cookie signature verification failed");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
395
481
|
function getAuth() {
|
|
396
482
|
return {
|
|
397
483
|
verifyIdToken
|
|
@@ -674,78 +760,6 @@ function removeFieldTransforms(data) {
|
|
|
674
760
|
return result;
|
|
675
761
|
}
|
|
676
762
|
|
|
677
|
-
// src/token-generation.ts
|
|
678
|
-
function base64UrlEncode2(str) {
|
|
679
|
-
return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
680
|
-
}
|
|
681
|
-
function base64UrlEncodeBuffer(buffer) {
|
|
682
|
-
return btoa(String.fromCharCode(...buffer)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
683
|
-
}
|
|
684
|
-
async function createJWT(serviceAccount) {
|
|
685
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
686
|
-
const expiry = now + 3600;
|
|
687
|
-
const header = {
|
|
688
|
-
alg: "RS256",
|
|
689
|
-
typ: "JWT"
|
|
690
|
-
};
|
|
691
|
-
const payload = {
|
|
692
|
-
iss: serviceAccount.client_email,
|
|
693
|
-
sub: serviceAccount.client_email,
|
|
694
|
-
aud: serviceAccount.token_uri,
|
|
695
|
-
iat: now,
|
|
696
|
-
exp: expiry,
|
|
697
|
-
scope: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/firebase"
|
|
698
|
-
};
|
|
699
|
-
const encodedHeader = base64UrlEncode2(JSON.stringify(header));
|
|
700
|
-
const encodedPayload = base64UrlEncode2(JSON.stringify(payload));
|
|
701
|
-
const unsignedToken = `${encodedHeader}.${encodedPayload}`;
|
|
702
|
-
const pemContents = serviceAccount.private_key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "");
|
|
703
|
-
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
|
|
704
|
-
const cryptoKey = await crypto.subtle.importKey(
|
|
705
|
-
"pkcs8",
|
|
706
|
-
binaryDer,
|
|
707
|
-
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
708
|
-
false,
|
|
709
|
-
["sign"]
|
|
710
|
-
);
|
|
711
|
-
const signature = await crypto.subtle.sign(
|
|
712
|
-
"RSASSA-PKCS1-v1_5",
|
|
713
|
-
cryptoKey,
|
|
714
|
-
new TextEncoder().encode(unsignedToken)
|
|
715
|
-
);
|
|
716
|
-
const encodedSignature = base64UrlEncodeBuffer(new Uint8Array(signature));
|
|
717
|
-
return `${unsignedToken}.${encodedSignature}`;
|
|
718
|
-
}
|
|
719
|
-
var cachedAccessToken = null;
|
|
720
|
-
var tokenExpiry = 0;
|
|
721
|
-
async function getAdminAccessToken() {
|
|
722
|
-
if (cachedAccessToken && Date.now() < tokenExpiry) {
|
|
723
|
-
return cachedAccessToken;
|
|
724
|
-
}
|
|
725
|
-
const serviceAccount = getServiceAccount();
|
|
726
|
-
const jwt = await createJWT(serviceAccount);
|
|
727
|
-
const response = await fetch(serviceAccount.token_uri, {
|
|
728
|
-
method: "POST",
|
|
729
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
730
|
-
body: new URLSearchParams({
|
|
731
|
-
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
732
|
-
assertion: jwt
|
|
733
|
-
})
|
|
734
|
-
});
|
|
735
|
-
if (!response.ok) {
|
|
736
|
-
const errorText = await response.text();
|
|
737
|
-
throw new Error(`Failed to get access token: ${errorText}`);
|
|
738
|
-
}
|
|
739
|
-
const data = await response.json();
|
|
740
|
-
cachedAccessToken = data.access_token;
|
|
741
|
-
tokenExpiry = Date.now() + data.expires_in * 1e3 - 6e4;
|
|
742
|
-
return cachedAccessToken;
|
|
743
|
-
}
|
|
744
|
-
function clearTokenCache() {
|
|
745
|
-
cachedAccessToken = null;
|
|
746
|
-
tokenExpiry = 0;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
763
|
// src/firestore/path-validation.ts
|
|
750
764
|
function validateCollectionPath(argumentName, collectionPath) {
|
|
751
765
|
const segments = collectionPath.split("/").filter((s) => s.length > 0);
|
|
@@ -1635,6 +1649,7 @@ export {
|
|
|
1635
1649
|
clearTokenCache,
|
|
1636
1650
|
countDocuments,
|
|
1637
1651
|
createCustomToken,
|
|
1652
|
+
createSessionCookie,
|
|
1638
1653
|
deleteDocument,
|
|
1639
1654
|
deleteFile,
|
|
1640
1655
|
downloadFile,
|
|
@@ -1658,5 +1673,6 @@ export {
|
|
|
1658
1673
|
updateDocument,
|
|
1659
1674
|
uploadFile,
|
|
1660
1675
|
uploadFileResumable,
|
|
1661
|
-
verifyIdToken
|
|
1676
|
+
verifyIdToken,
|
|
1677
|
+
verifySessionCookie
|
|
1662
1678
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prmichaelsen/firebase-admin-sdk-v8",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"description": "Firebase Admin SDK for Cloudflare Workers and edge runtimes using REST APIs",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"typecheck": "tsc --noEmit",
|
|
27
27
|
"test": "jest",
|
|
28
28
|
"test:watch": "jest --watch",
|
|
29
|
-
"test:e2e": "jest --config jest.e2e.config.js",
|
|
30
|
-
"test:e2e:watch": "jest --config jest.e2e.config.js --watch",
|
|
29
|
+
"test:e2e": "node --env-file=.env node_modules/.bin/jest --config jest.e2e.config.js",
|
|
30
|
+
"test:e2e:watch": "node --env-file=.env node_modules/.bin/jest --config jest.e2e.config.js --watch",
|
|
31
31
|
"test:all": "npm test && npm run test:e2e",
|
|
32
32
|
"prepublishOnly": "npm run build"
|
|
33
33
|
},
|