@tern-secure/backend 1.2.0-canary.v20251127235234 → 1.2.0-canary.v20251202164451
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/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/types.d.ts +42 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/admin/index.d.ts +1 -1
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +8 -1
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/index.mjs +24 -598
- package/dist/admin/index.mjs.map +1 -1
- package/dist/app-check/AppCheckApi.d.ts +14 -0
- package/dist/app-check/AppCheckApi.d.ts.map +1 -0
- package/dist/app-check/generator.d.ts +9 -0
- package/dist/app-check/generator.d.ts.map +1 -0
- package/dist/app-check/index.d.ts +18 -0
- package/dist/app-check/index.d.ts.map +1 -0
- package/dist/app-check/index.js +1135 -0
- package/dist/app-check/index.js.map +1 -0
- package/dist/app-check/index.mjs +13 -0
- package/dist/app-check/index.mjs.map +1 -0
- package/dist/app-check/serverAppCheck.d.ts +33 -0
- package/dist/app-check/serverAppCheck.d.ts.map +1 -0
- package/dist/app-check/types.d.ts +21 -0
- package/dist/app-check/types.d.ts.map +1 -0
- package/dist/app-check/verifier.d.ts +16 -0
- package/dist/app-check/verifier.d.ts.map +1 -0
- package/dist/auth/credential.d.ts +5 -5
- package/dist/auth/credential.d.ts.map +1 -1
- package/dist/auth/getauth.d.ts +2 -1
- package/dist/auth/getauth.d.ts.map +1 -1
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +902 -394
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/index.mjs +5 -3
- package/dist/chunk-34QENCWP.mjs +784 -0
- package/dist/chunk-34QENCWP.mjs.map +1 -0
- package/dist/{chunk-NXYWC6YO.mjs → chunk-TUYCJY35.mjs} +182 -6
- package/dist/chunk-TUYCJY35.mjs.map +1 -0
- package/dist/chunk-UCSJDX6Y.mjs +778 -0
- package/dist/chunk-UCSJDX6Y.mjs.map +1 -0
- package/dist/constants.d.ts +10 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/fireRestApi/endpoints/AppCheckApi.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1275 -856
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +97 -137
- package/dist/index.mjs.map +1 -1
- package/dist/jwt/crypto-signer.d.ts +21 -0
- package/dist/jwt/crypto-signer.d.ts.map +1 -0
- package/dist/jwt/index.d.ts +2 -1
- package/dist/jwt/index.d.ts.map +1 -1
- package/dist/jwt/index.js +119 -2
- package/dist/jwt/index.js.map +1 -1
- package/dist/jwt/index.mjs +7 -3
- package/dist/jwt/signJwt.d.ts +8 -2
- package/dist/jwt/signJwt.d.ts.map +1 -1
- package/dist/jwt/types.d.ts +6 -0
- package/dist/jwt/types.d.ts.map +1 -1
- package/dist/jwt/verifyJwt.d.ts +7 -1
- package/dist/jwt/verifyJwt.d.ts.map +1 -1
- package/dist/tokens/authstate.d.ts +2 -0
- package/dist/tokens/authstate.d.ts.map +1 -1
- package/dist/tokens/c-authenticateRequestProcessor.d.ts +2 -2
- package/dist/tokens/c-authenticateRequestProcessor.d.ts.map +1 -1
- package/dist/tokens/keys.d.ts.map +1 -1
- package/dist/tokens/request.d.ts.map +1 -1
- package/dist/tokens/types.d.ts +6 -4
- package/dist/tokens/types.d.ts.map +1 -1
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/{auth/utils.d.ts → utils/fetcher.d.ts} +2 -1
- package/dist/utils/fetcher.d.ts.map +1 -0
- package/dist/utils/mapDecode.d.ts +2 -1
- package/dist/utils/mapDecode.d.ts.map +1 -1
- package/dist/utils/token-generator.d.ts +4 -0
- package/dist/utils/token-generator.d.ts.map +1 -0
- package/package.json +13 -3
- package/dist/auth/constants.d.ts +0 -6
- package/dist/auth/constants.d.ts.map +0 -1
- package/dist/auth/utils.d.ts.map +0 -1
- package/dist/chunk-DJLDUW7J.mjs +0 -414
- package/dist/chunk-DJLDUW7J.mjs.map +0 -1
- package/dist/chunk-GFH5CXQR.mjs +0 -71
- package/dist/chunk-GFH5CXQR.mjs.map +0 -1
- package/dist/chunk-NXYWC6YO.mjs.map +0 -1
- package/dist/chunk-WIVOBOZR.mjs +0 -86
- package/dist/chunk-WIVOBOZR.mjs.map +0 -1
- package/dist/utils/gemini_admin-init.d.ts +0 -10
- package/dist/utils/gemini_admin-init.d.ts.map +0 -1
package/dist/auth/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,112 +17,37 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/auth/index.ts
|
|
21
31
|
var auth_exports = {};
|
|
22
32
|
__export(auth_exports, {
|
|
33
|
+
ServiceAccountManager: () => ServiceAccountManager,
|
|
23
34
|
getAuth: () => getAuth
|
|
24
35
|
});
|
|
25
36
|
module.exports = __toCommonJS(auth_exports);
|
|
26
37
|
|
|
27
|
-
// src/jwt/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
this.name = "CustomTokenError";
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
var RESERVED_CLAIMS = [
|
|
37
|
-
"acr",
|
|
38
|
-
"amr",
|
|
39
|
-
"at_hash",
|
|
40
|
-
"aud",
|
|
41
|
-
"auth_time",
|
|
42
|
-
"azp",
|
|
43
|
-
"cnf",
|
|
44
|
-
"c_hash",
|
|
45
|
-
"exp",
|
|
46
|
-
"firebase",
|
|
47
|
-
"iat",
|
|
48
|
-
"iss",
|
|
49
|
-
"jti",
|
|
50
|
-
"nbf",
|
|
51
|
-
"nonce",
|
|
52
|
-
"sub"
|
|
53
|
-
];
|
|
54
|
-
async function createCustomTokenJwt(uid, developerClaims) {
|
|
55
|
-
try {
|
|
56
|
-
const privateKey = process.env.FIREBASE_PRIVATE_KEY;
|
|
57
|
-
const clientEmail = process.env.FIREBASE_CLIENT_EMAIL;
|
|
58
|
-
if (!privateKey || !clientEmail) {
|
|
59
|
-
return {
|
|
60
|
-
errors: [
|
|
61
|
-
new CustomTokenError(
|
|
62
|
-
"Missing FIREBASE_PRIVATE_KEY or FIREBASE_CLIENT_EMAIL environment variables",
|
|
63
|
-
"MISSING_ENV_VARS"
|
|
64
|
-
)
|
|
65
|
-
]
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
if (!uid || typeof uid !== "string") {
|
|
69
|
-
return {
|
|
70
|
-
errors: [new CustomTokenError("uid must be a non-empty string", "INVALID_UID")]
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
if (uid.length > 128) {
|
|
74
|
-
return {
|
|
75
|
-
errors: [new CustomTokenError("uid must not exceed 128 characters", "UID_TOO_LONG")]
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
if (developerClaims) {
|
|
79
|
-
for (const claim of Object.keys(developerClaims)) {
|
|
80
|
-
if (RESERVED_CLAIMS.includes(claim)) {
|
|
81
|
-
return {
|
|
82
|
-
errors: [new CustomTokenError(`Custom claim '${claim}' is reserved`, "RESERVED_CLAIM")]
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
38
|
+
// src/jwt/guardReturn.ts
|
|
39
|
+
function createJwtGuard(decodedFn) {
|
|
40
|
+
return (...args) => {
|
|
41
|
+
const { data, errors } = decodedFn(...args);
|
|
42
|
+
if (errors) {
|
|
43
|
+
throw errors[0];
|
|
86
44
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const parsedPrivateKey = await (0, import_jose.importPKCS8)(privateKey.replace(/\\n/g, "\n"), "RS256");
|
|
90
|
-
const payload = {
|
|
91
|
-
iss: clientEmail,
|
|
92
|
-
sub: clientEmail,
|
|
93
|
-
aud: "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
|
|
94
|
-
iat: now,
|
|
95
|
-
exp: now + expiresIn,
|
|
96
|
-
uid,
|
|
97
|
-
...developerClaims
|
|
98
|
-
};
|
|
99
|
-
const jwt = await new import_jose.SignJWT(payload).setProtectedHeader({ alg: "RS256", typ: "JWT" }).setIssuedAt(now).setExpirationTime(now + expiresIn).setIssuer(clientEmail).setSubject(clientEmail).setAudience(
|
|
100
|
-
"https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"
|
|
101
|
-
).sign(parsedPrivateKey);
|
|
102
|
-
return {
|
|
103
|
-
data: jwt
|
|
104
|
-
};
|
|
105
|
-
} catch (error) {
|
|
106
|
-
const message = error instanceof Error ? error.message : "Unknown error occurred";
|
|
107
|
-
return {
|
|
108
|
-
errors: [
|
|
109
|
-
new CustomTokenError(`Failed to create custom token: ${message}`, "TOKEN_CREATION_FAILED")
|
|
110
|
-
]
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
async function createCustomToken(uid, developerClaims) {
|
|
115
|
-
const { data, errors } = await createCustomTokenJwt(uid, developerClaims);
|
|
116
|
-
if (errors) {
|
|
117
|
-
throw errors[0];
|
|
118
|
-
}
|
|
119
|
-
return data;
|
|
45
|
+
return data;
|
|
46
|
+
};
|
|
120
47
|
}
|
|
121
48
|
|
|
122
49
|
// src/jwt/verifyJwt.ts
|
|
123
|
-
var
|
|
50
|
+
var import_jose2 = require("jose");
|
|
124
51
|
|
|
125
52
|
// src/utils/errors.ts
|
|
126
53
|
var TokenVerificationErrorReason = {
|
|
@@ -163,6 +90,11 @@ function mapJwtPayloadToDecodedIdToken(payload) {
|
|
|
163
90
|
decodedIdToken.uid = decodedIdToken.sub;
|
|
164
91
|
return decodedIdToken;
|
|
165
92
|
}
|
|
93
|
+
function mapJwtPayloadToDecodedAppCheckToken(payload) {
|
|
94
|
+
const decodedAppCheckToken = payload;
|
|
95
|
+
decodedAppCheckToken.app_id = decodedAppCheckToken.sub;
|
|
96
|
+
return decodedAppCheckToken;
|
|
97
|
+
}
|
|
166
98
|
|
|
167
99
|
// src/utils/rfc4648.ts
|
|
168
100
|
var base64url = {
|
|
@@ -241,10 +173,10 @@ function stringify(data, encoding, opts = {}) {
|
|
|
241
173
|
}
|
|
242
174
|
|
|
243
175
|
// src/jwt/cryptoKeys.ts
|
|
244
|
-
var
|
|
176
|
+
var import_jose = require("jose");
|
|
245
177
|
async function importKey(key, algorithm) {
|
|
246
178
|
if (typeof key === "object") {
|
|
247
|
-
const result = await (0,
|
|
179
|
+
const result = await (0, import_jose.importJWK)(key, algorithm);
|
|
248
180
|
if (result instanceof Uint8Array) {
|
|
249
181
|
throw new Error("Unexpected Uint8Array result from JWK import");
|
|
250
182
|
}
|
|
@@ -252,13 +184,13 @@ async function importKey(key, algorithm) {
|
|
|
252
184
|
}
|
|
253
185
|
const keyString = key.trim();
|
|
254
186
|
if (keyString.includes("-----BEGIN CERTIFICATE-----")) {
|
|
255
|
-
return await (0,
|
|
187
|
+
return await (0, import_jose.importX509)(keyString, algorithm);
|
|
256
188
|
}
|
|
257
189
|
if (keyString.includes("-----BEGIN PUBLIC KEY-----")) {
|
|
258
|
-
return await (0,
|
|
190
|
+
return await (0, import_jose.importSPKI)(keyString, algorithm);
|
|
259
191
|
}
|
|
260
192
|
try {
|
|
261
|
-
return await (0,
|
|
193
|
+
return await (0, import_jose.importSPKI)(keyString, algorithm);
|
|
262
194
|
} catch (error) {
|
|
263
195
|
throw new Error(
|
|
264
196
|
`Unsupported key format. Supported formats: X.509 certificate (PEM), SPKI (PEM), JWK (JSON object or string). Error: ${error}`
|
|
@@ -341,7 +273,7 @@ async function verifySignature(jwt, key) {
|
|
|
341
273
|
const joseAlgorithm = header.alg || "RS256";
|
|
342
274
|
try {
|
|
343
275
|
const publicKey = await importKey(key, joseAlgorithm);
|
|
344
|
-
const { payload } = await (0,
|
|
276
|
+
const { payload } = await (0, import_jose2.jwtVerify)(raw.text, publicKey);
|
|
345
277
|
return { data: payload };
|
|
346
278
|
} catch (error) {
|
|
347
279
|
return {
|
|
@@ -356,8 +288,8 @@ async function verifySignature(jwt, key) {
|
|
|
356
288
|
}
|
|
357
289
|
function ternDecodeJwt(token) {
|
|
358
290
|
try {
|
|
359
|
-
const header = (0,
|
|
360
|
-
const payload = (0,
|
|
291
|
+
const header = (0, import_jose2.decodeProtectedHeader)(token);
|
|
292
|
+
const payload = (0, import_jose2.decodeJwt)(token);
|
|
361
293
|
const tokenParts = (token || "").toString().split(".");
|
|
362
294
|
if (tokenParts.length !== 3) {
|
|
363
295
|
return {
|
|
@@ -424,179 +356,189 @@ async function verifyJwt(token, options) {
|
|
|
424
356
|
const decodedIdToken = mapJwtPayloadToDecodedIdToken(verifiedPayload);
|
|
425
357
|
return { data: decodedIdToken };
|
|
426
358
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
HandshakeNonce: "__ternsecure_handshake_nonce"
|
|
444
|
-
};
|
|
445
|
-
var QueryParameters = {
|
|
446
|
-
TernSynced: "__tern_synced",
|
|
447
|
-
SuffixedCookies: "suffixed_cookies",
|
|
448
|
-
TernRedirectUrl: "__tern_redirect_url",
|
|
449
|
-
// use the reference to Cookies to indicate that it's the same value
|
|
450
|
-
DevBrowser: Cookies.DevBrowser,
|
|
451
|
-
Handshake: Cookies.Handshake,
|
|
452
|
-
HandshakeHelp: "__tern_help",
|
|
453
|
-
LegacyDevBrowser: "__dev_session",
|
|
454
|
-
HandshakeReason: "__tern_hs_reason",
|
|
455
|
-
HandshakeNonce: Cookies.HandshakeNonce
|
|
456
|
-
};
|
|
457
|
-
|
|
458
|
-
// src/tokens/keys.ts
|
|
459
|
-
var cache = {};
|
|
460
|
-
var lastUpdatedAt = 0;
|
|
461
|
-
var googleExpiresAt = 0;
|
|
462
|
-
function getFromCache(kid) {
|
|
463
|
-
return cache[kid];
|
|
464
|
-
}
|
|
465
|
-
function getCacheValues() {
|
|
466
|
-
return Object.values(cache);
|
|
467
|
-
}
|
|
468
|
-
function setInCache(kid, certificate, shouldExpire = true) {
|
|
469
|
-
cache[kid] = certificate;
|
|
470
|
-
lastUpdatedAt = shouldExpire ? Date.now() : -1;
|
|
471
|
-
}
|
|
472
|
-
async function fetchPublicKeys(keyUrl) {
|
|
473
|
-
const url = new URL(keyUrl);
|
|
474
|
-
const response = await fetch(url);
|
|
475
|
-
if (!response.ok) {
|
|
476
|
-
throw new TokenVerificationError({
|
|
477
|
-
message: `Error loading public keys from ${url.href} with code=${response.status} `,
|
|
478
|
-
reason: TokenVerificationErrorReason.TokenInvalid
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
const data = await response.json();
|
|
482
|
-
const expiresAt = getExpiresAt(response);
|
|
483
|
-
return {
|
|
484
|
-
keys: data,
|
|
485
|
-
expiresAt
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
async function loadJWKFromRemote({
|
|
489
|
-
keyURL = GOOGLE_PUBLIC_KEYS_URL,
|
|
490
|
-
skipJwksCache,
|
|
491
|
-
kid
|
|
492
|
-
}) {
|
|
493
|
-
if (skipJwksCache || isCacheExpired() || !getFromCache(kid)) {
|
|
494
|
-
const { keys, expiresAt } = await fetchPublicKeys(keyURL);
|
|
495
|
-
if (!keys || Object.keys(keys).length === 0) {
|
|
496
|
-
throw new TokenVerificationError({
|
|
497
|
-
message: `The JWKS endpoint ${keyURL} returned no keys`,
|
|
498
|
-
reason: TokenVerificationErrorReason.RemoteJWKFailedToLoad
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
googleExpiresAt = expiresAt;
|
|
502
|
-
Object.entries(keys).forEach(([keyId, cert2]) => {
|
|
503
|
-
setInCache(keyId, cert2);
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
const cert = getFromCache(kid);
|
|
507
|
-
if (!cert) {
|
|
508
|
-
getCacheValues();
|
|
509
|
-
const availableKids = Object.keys(cache).sort().join(", ");
|
|
510
|
-
throw new TokenVerificationError({
|
|
511
|
-
message: `No public key found for kid "${kid}". Available kids: [${availableKids}]`,
|
|
512
|
-
reason: TokenVerificationErrorReason.TokenInvalid
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
return cert;
|
|
516
|
-
}
|
|
517
|
-
function isCacheExpired() {
|
|
518
|
-
const now = Date.now();
|
|
519
|
-
if (lastUpdatedAt === -1) {
|
|
520
|
-
return false;
|
|
521
|
-
}
|
|
522
|
-
const cacheAge = now - lastUpdatedAt;
|
|
523
|
-
const maxCacheAge = MAX_CACHE_LAST_UPDATED_AT_SECONDS * 1e3;
|
|
524
|
-
const localCacheExpired = cacheAge >= maxCacheAge;
|
|
525
|
-
const googleCacheExpired = now >= googleExpiresAt;
|
|
526
|
-
const isExpired = localCacheExpired || googleCacheExpired;
|
|
527
|
-
if (isExpired) {
|
|
528
|
-
cache = {};
|
|
529
|
-
}
|
|
530
|
-
return isExpired;
|
|
531
|
-
}
|
|
532
|
-
function getExpiresAt(res) {
|
|
533
|
-
const cacheControlHeader = res.headers.get("cache-control");
|
|
534
|
-
if (!cacheControlHeader) {
|
|
535
|
-
return Date.now() + DEFAULT_CACHE_DURATION;
|
|
359
|
+
async function verifyAppCheckSignature(jwt, getPublicKey2) {
|
|
360
|
+
const { header, raw } = jwt;
|
|
361
|
+
const joseAlgorithm = header.alg || "RS256";
|
|
362
|
+
try {
|
|
363
|
+
const key = await getPublicKey2();
|
|
364
|
+
const { payload } = await (0, import_jose2.jwtVerify)(raw.text, key);
|
|
365
|
+
return { data: payload };
|
|
366
|
+
} catch (error) {
|
|
367
|
+
return {
|
|
368
|
+
errors: [
|
|
369
|
+
new TokenVerificationError({
|
|
370
|
+
reason: TokenVerificationErrorReason.TokenInvalidSignature,
|
|
371
|
+
message: error.message
|
|
372
|
+
})
|
|
373
|
+
]
|
|
374
|
+
};
|
|
536
375
|
}
|
|
537
|
-
const maxAgeMatch = cacheControlHeader.match(CACHE_CONTROL_REGEX);
|
|
538
|
-
const maxAge = maxAgeMatch ? parseInt(maxAgeMatch[1], 10) : DEFAULT_CACHE_DURATION / 1e3;
|
|
539
|
-
return Date.now() + maxAge * 1e3;
|
|
540
376
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const { data:
|
|
377
|
+
async function verifyAppCheckJwt(token, options) {
|
|
378
|
+
const { key: getPublicKey2 } = options;
|
|
379
|
+
const clockSkew = options.clockSkewInMs || DEFAULT_CLOCK_SKEW_IN_MS;
|
|
380
|
+
const { data: decoded, errors } = ternDecodeJwt(token);
|
|
545
381
|
if (errors) {
|
|
546
382
|
return { errors };
|
|
547
383
|
}
|
|
548
|
-
const { header } =
|
|
549
|
-
|
|
550
|
-
|
|
384
|
+
const { header, payload } = decoded;
|
|
385
|
+
try {
|
|
386
|
+
verifyHeaderKid(header.kid);
|
|
387
|
+
verifySubClaim(payload.sub);
|
|
388
|
+
verifyExpirationClaim(payload.exp, clockSkew);
|
|
389
|
+
verifyIssuedAtClaim(payload.iat, clockSkew);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
return { errors: [error] };
|
|
392
|
+
}
|
|
393
|
+
const { data: verifiedPayload, errors: signatureErrors } = await verifyAppCheckSignature(
|
|
394
|
+
decoded,
|
|
395
|
+
getPublicKey2
|
|
396
|
+
);
|
|
397
|
+
if (signatureErrors) {
|
|
551
398
|
return {
|
|
552
399
|
errors: [
|
|
553
400
|
new TokenVerificationError({
|
|
554
|
-
reason: TokenVerificationErrorReason.
|
|
555
|
-
message:
|
|
401
|
+
reason: TokenVerificationErrorReason.TokenInvalidSignature,
|
|
402
|
+
message: "Token signature verification failed."
|
|
556
403
|
})
|
|
557
404
|
]
|
|
558
405
|
};
|
|
559
406
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
407
|
+
const decodedAppCheckToken = mapJwtPayloadToDecodedAppCheckToken(verifiedPayload);
|
|
408
|
+
return { data: decodedAppCheckToken };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/jwt/jwt.ts
|
|
412
|
+
var import_jose3 = require("jose");
|
|
413
|
+
|
|
414
|
+
// src/jwt/customJwt.ts
|
|
415
|
+
var import_jose4 = require("jose");
|
|
416
|
+
var CustomTokenError = class extends Error {
|
|
417
|
+
constructor(message, code) {
|
|
418
|
+
super(message);
|
|
419
|
+
this.code = code;
|
|
420
|
+
this.name = "CustomTokenError";
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
var RESERVED_CLAIMS = [
|
|
424
|
+
"acr",
|
|
425
|
+
"amr",
|
|
426
|
+
"at_hash",
|
|
427
|
+
"aud",
|
|
428
|
+
"auth_time",
|
|
429
|
+
"azp",
|
|
430
|
+
"cnf",
|
|
431
|
+
"c_hash",
|
|
432
|
+
"exp",
|
|
433
|
+
"firebase",
|
|
434
|
+
"iat",
|
|
435
|
+
"iss",
|
|
436
|
+
"jti",
|
|
437
|
+
"nbf",
|
|
438
|
+
"nonce",
|
|
439
|
+
"sub"
|
|
440
|
+
];
|
|
441
|
+
async function createCustomTokenJwt(uid, developerClaims) {
|
|
442
|
+
try {
|
|
443
|
+
const privateKey = process.env.FIREBASE_PRIVATE_KEY;
|
|
444
|
+
const clientEmail = process.env.FIREBASE_CLIENT_EMAIL;
|
|
445
|
+
if (!privateKey || !clientEmail) {
|
|
446
|
+
return {
|
|
447
|
+
errors: [
|
|
448
|
+
new CustomTokenError(
|
|
449
|
+
"Missing FIREBASE_PRIVATE_KEY or FIREBASE_CLIENT_EMAIL environment variables",
|
|
450
|
+
"MISSING_ENV_VARS"
|
|
451
|
+
)
|
|
569
452
|
]
|
|
570
453
|
};
|
|
571
454
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
455
|
+
if (!uid || typeof uid !== "string") {
|
|
456
|
+
return {
|
|
457
|
+
errors: [new CustomTokenError("uid must be a non-empty string", "INVALID_UID")]
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
if (uid.length > 128) {
|
|
461
|
+
return {
|
|
462
|
+
errors: [new CustomTokenError("uid must not exceed 128 characters", "UID_TOO_LONG")]
|
|
463
|
+
};
|
|
576
464
|
}
|
|
465
|
+
if (developerClaims) {
|
|
466
|
+
for (const claim of Object.keys(developerClaims)) {
|
|
467
|
+
if (RESERVED_CLAIMS.includes(claim)) {
|
|
468
|
+
return {
|
|
469
|
+
errors: [new CustomTokenError(`Custom claim '${claim}' is reserved`, "RESERVED_CLAIM")]
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const expiresIn = 3600;
|
|
475
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
476
|
+
const parsedPrivateKey = await (0, import_jose4.importPKCS8)(privateKey.replace(/\\n/g, "\n"), "RS256");
|
|
477
|
+
const payload = {
|
|
478
|
+
iss: clientEmail,
|
|
479
|
+
sub: clientEmail,
|
|
480
|
+
aud: "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
|
|
481
|
+
iat: now,
|
|
482
|
+
exp: now + expiresIn,
|
|
483
|
+
uid,
|
|
484
|
+
...developerClaims
|
|
485
|
+
};
|
|
486
|
+
const jwt = await new import_jose4.SignJWT(payload).setProtectedHeader({ alg: "RS256", typ: "JWT" }).setIssuedAt(now).setExpirationTime(now + expiresIn).setIssuer(clientEmail).setSubject(clientEmail).setAudience(
|
|
487
|
+
"https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"
|
|
488
|
+
).sign(parsedPrivateKey);
|
|
577
489
|
return {
|
|
578
|
-
|
|
490
|
+
data: jwt
|
|
491
|
+
};
|
|
492
|
+
} catch (error) {
|
|
493
|
+
const message = error instanceof Error ? error.message : "Unknown error occurred";
|
|
494
|
+
return {
|
|
495
|
+
errors: [
|
|
496
|
+
new CustomTokenError(`Failed to create custom token: ${message}`, "TOKEN_CREATION_FAILED")
|
|
497
|
+
]
|
|
579
498
|
};
|
|
580
499
|
}
|
|
581
500
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
throw errors[0];
|
|
589
|
-
}
|
|
590
|
-
return data;
|
|
591
|
-
};
|
|
501
|
+
async function createCustomToken(uid, developerClaims) {
|
|
502
|
+
const { data, errors } = await createCustomTokenJwt(uid, developerClaims);
|
|
503
|
+
if (errors) {
|
|
504
|
+
throw errors[0];
|
|
505
|
+
}
|
|
506
|
+
return data;
|
|
592
507
|
}
|
|
593
508
|
|
|
594
|
-
// src/jwt/jwt.ts
|
|
595
|
-
var import_jose4 = require("jose");
|
|
596
|
-
|
|
597
509
|
// src/jwt/signJwt.ts
|
|
598
510
|
var import_jose5 = require("jose");
|
|
511
|
+
|
|
512
|
+
// src/utils/fetcher.ts
|
|
513
|
+
async function getDetailFromResponse(response) {
|
|
514
|
+
const json = await response.json();
|
|
515
|
+
if (!json) {
|
|
516
|
+
return "Missing error payload";
|
|
517
|
+
}
|
|
518
|
+
let detail = typeof json.error === "string" ? json.error : json.error?.message ?? "Missing error payload";
|
|
519
|
+
if (json.error_description) {
|
|
520
|
+
detail += " (" + json.error_description + ")";
|
|
521
|
+
}
|
|
522
|
+
return detail;
|
|
523
|
+
}
|
|
524
|
+
async function fetchText(url, init) {
|
|
525
|
+
return (await fetchAny(url, init)).text();
|
|
526
|
+
}
|
|
527
|
+
async function fetchJson(url, init) {
|
|
528
|
+
return (await fetchAny(url, init)).json();
|
|
529
|
+
}
|
|
530
|
+
async function fetchAny(url, init) {
|
|
531
|
+
const response = await fetch(url, init);
|
|
532
|
+
if (!response.ok) {
|
|
533
|
+
throw new Error(await getDetailFromResponse(response));
|
|
534
|
+
}
|
|
535
|
+
return response;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/jwt/types.ts
|
|
599
539
|
var ALGORITHM_RS256 = "RS256";
|
|
540
|
+
|
|
541
|
+
// src/jwt/signJwt.ts
|
|
600
542
|
async function ternSignJwt(opts) {
|
|
601
543
|
const { payload, privateKey, keyId } = opts;
|
|
602
544
|
let key;
|
|
@@ -604,118 +546,636 @@ async function ternSignJwt(opts) {
|
|
|
604
546
|
key = await (0, import_jose5.importPKCS8)(privateKey, ALGORITHM_RS256);
|
|
605
547
|
} catch (error) {
|
|
606
548
|
throw new TokenVerificationError({
|
|
607
|
-
message: `Failed to import private key: ${error.message}`,
|
|
549
|
+
message: `Failed to import private key: ${error.message}`,
|
|
550
|
+
reason: TokenVerificationErrorReason.TokenInvalid
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
return new import_jose5.SignJWT(payload).setProtectedHeader({ alg: ALGORITHM_RS256, kid: keyId }).sign(key);
|
|
554
|
+
}
|
|
555
|
+
function formatBase64(value) {
|
|
556
|
+
return value.replace(/\//g, "_").replace(/\+/g, "-").replace(/=+$/, "");
|
|
557
|
+
}
|
|
558
|
+
function encodeSegment(segment) {
|
|
559
|
+
const value = JSON.stringify(segment);
|
|
560
|
+
return formatBase64(import_jose5.base64url.encode(value));
|
|
561
|
+
}
|
|
562
|
+
async function ternSignBlob({
|
|
563
|
+
payload,
|
|
564
|
+
serviceAccountId,
|
|
565
|
+
accessToken
|
|
566
|
+
}) {
|
|
567
|
+
const url = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccountId}:signBlob`;
|
|
568
|
+
const header = {
|
|
569
|
+
alg: ALGORITHM_RS256,
|
|
570
|
+
typ: "JWT"
|
|
571
|
+
};
|
|
572
|
+
const token = `${encodeSegment(header)}.${encodeSegment(payload)}`;
|
|
573
|
+
const request = {
|
|
574
|
+
method: "POST",
|
|
575
|
+
headers: {
|
|
576
|
+
Authorization: `Bearer ${accessToken}`
|
|
577
|
+
},
|
|
578
|
+
body: JSON.stringify({ payload: import_jose5.base64url.encode(token) })
|
|
579
|
+
};
|
|
580
|
+
const response = await fetchAny(url, request);
|
|
581
|
+
const blob = await response.blob();
|
|
582
|
+
const key = await blob.text();
|
|
583
|
+
const { signedBlob } = JSON.parse(key);
|
|
584
|
+
return `${token}.${formatBase64(signedBlob)}`;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/jwt/crypto-signer.ts
|
|
588
|
+
var ServiceAccountSigner = class {
|
|
589
|
+
constructor(credential, tenantId) {
|
|
590
|
+
this.credential = credential;
|
|
591
|
+
this.tenantId = tenantId;
|
|
592
|
+
}
|
|
593
|
+
async getAccountId() {
|
|
594
|
+
return Promise.resolve(this.credential.clientEmail);
|
|
595
|
+
}
|
|
596
|
+
async sign(payload) {
|
|
597
|
+
if (this.tenantId) {
|
|
598
|
+
payload.tenant_id = this.tenantId;
|
|
599
|
+
}
|
|
600
|
+
return ternSignJwt({ payload, privateKey: this.credential.privateKey });
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
var IAMSigner = class {
|
|
604
|
+
algorithm = ALGORITHM_RS256;
|
|
605
|
+
credential;
|
|
606
|
+
tenantId;
|
|
607
|
+
serviceAccountId;
|
|
608
|
+
constructor(credential, tenantId, serviceAccountId) {
|
|
609
|
+
this.credential = credential;
|
|
610
|
+
this.tenantId = tenantId;
|
|
611
|
+
this.serviceAccountId = serviceAccountId;
|
|
612
|
+
}
|
|
613
|
+
async sign(payload) {
|
|
614
|
+
if (this.tenantId) {
|
|
615
|
+
payload.tenant_id = this.tenantId;
|
|
616
|
+
}
|
|
617
|
+
const serviceAccount = await this.getAccountId();
|
|
618
|
+
const accessToken = await this.credential.getAccessToken();
|
|
619
|
+
return ternSignBlob({
|
|
620
|
+
accessToken: accessToken.accessToken,
|
|
621
|
+
serviceAccountId: serviceAccount,
|
|
622
|
+
payload
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
async getAccountId() {
|
|
626
|
+
if (this.serviceAccountId) {
|
|
627
|
+
return this.serviceAccountId;
|
|
628
|
+
}
|
|
629
|
+
const token = await this.credential.getAccessToken();
|
|
630
|
+
const url = "http://metadata/computeMetadata/v1/instance/service-accounts/default/email";
|
|
631
|
+
const request = {
|
|
632
|
+
method: "GET",
|
|
633
|
+
headers: {
|
|
634
|
+
"Metadata-Flavor": "Google",
|
|
635
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
return this.serviceAccountId = await fetchText(url, request);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// src/jwt/index.ts
|
|
643
|
+
var ternDecodeJwt2 = createJwtGuard(ternDecodeJwt);
|
|
644
|
+
|
|
645
|
+
// src/utils/token-generator.ts
|
|
646
|
+
function cryptoSignerFromCredential(credential, tenantId, serviceAccountId) {
|
|
647
|
+
if (credential instanceof ServiceAccountManager) {
|
|
648
|
+
return new ServiceAccountSigner(credential, tenantId);
|
|
649
|
+
}
|
|
650
|
+
return new IAMSigner(credential, tenantId, serviceAccountId);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/app-check/AppCheckApi.ts
|
|
654
|
+
function getSdkVersion() {
|
|
655
|
+
return "12.7.0";
|
|
656
|
+
}
|
|
657
|
+
var FIREBASE_APP_CHECK_CONFIG_HEADERS = {
|
|
658
|
+
"X-Firebase-Client": `fire-admin-node/${getSdkVersion()}`
|
|
659
|
+
};
|
|
660
|
+
var AppCheckApi = class {
|
|
661
|
+
constructor(credential) {
|
|
662
|
+
this.credential = credential;
|
|
663
|
+
}
|
|
664
|
+
async exchangeToken(params) {
|
|
665
|
+
const { projectId, appId, customToken, limitedUse = false } = params;
|
|
666
|
+
const token = await this.credential.getAccessToken(false);
|
|
667
|
+
if (!projectId || !appId) {
|
|
668
|
+
throw new Error("Project ID and App ID are required for App Check token exchange");
|
|
669
|
+
}
|
|
670
|
+
const endpoint = `https://firebaseappcheck.googleapis.com/v1/projects/${projectId}/apps/${appId}:exchangeCustomToken`;
|
|
671
|
+
const headers = {
|
|
672
|
+
"Content-Type": "application/json",
|
|
673
|
+
"Authorization": `Bearer ${token.accessToken}`
|
|
674
|
+
};
|
|
675
|
+
try {
|
|
676
|
+
const response = await fetch(endpoint, {
|
|
677
|
+
method: "POST",
|
|
678
|
+
headers,
|
|
679
|
+
body: JSON.stringify({ customToken, limitedUse })
|
|
680
|
+
});
|
|
681
|
+
if (!response.ok) {
|
|
682
|
+
const errorText = await response.text();
|
|
683
|
+
throw new Error(`App Check token exchange failed: ${response.status} ${errorText}`);
|
|
684
|
+
}
|
|
685
|
+
const data = await response.json();
|
|
686
|
+
return {
|
|
687
|
+
token: data.token,
|
|
688
|
+
ttl: data.ttl
|
|
689
|
+
};
|
|
690
|
+
} catch (error) {
|
|
691
|
+
console.warn("[ternsecure - appcheck api]unexpected error:", error);
|
|
692
|
+
throw error;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
async exchangeDebugToken(params) {
|
|
696
|
+
const { projectId, appId, customToken, accessToken, limitedUse = false } = params;
|
|
697
|
+
if (!projectId || !appId) {
|
|
698
|
+
throw new Error("Project ID and App ID are required for App Check token exchange");
|
|
699
|
+
}
|
|
700
|
+
const endpoint = `https://firebaseappcheck.googleapis.com/v1beta/projects/${projectId}/apps/${appId}:exchangeDebugToken`;
|
|
701
|
+
const headers = {
|
|
702
|
+
...FIREBASE_APP_CHECK_CONFIG_HEADERS,
|
|
703
|
+
"Authorization": `Bearer ${accessToken}`
|
|
704
|
+
};
|
|
705
|
+
const body = {
|
|
706
|
+
customToken,
|
|
707
|
+
limitedUse
|
|
708
|
+
};
|
|
709
|
+
try {
|
|
710
|
+
const response = await fetch(endpoint, {
|
|
711
|
+
method: "POST",
|
|
712
|
+
headers,
|
|
713
|
+
body: JSON.stringify(body)
|
|
714
|
+
});
|
|
715
|
+
if (!response.ok) {
|
|
716
|
+
const errorText = await response.text();
|
|
717
|
+
throw new Error(`App Check token exchange failed: ${response.status} ${errorText}`);
|
|
718
|
+
}
|
|
719
|
+
const data = await response.json();
|
|
720
|
+
return {
|
|
721
|
+
token: data.token,
|
|
722
|
+
ttl: data.ttl
|
|
723
|
+
};
|
|
724
|
+
} catch (error) {
|
|
725
|
+
console.warn("[ternsecure - appcheck api]unexpected error:", error);
|
|
726
|
+
throw error;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
// src/constants.ts
|
|
732
|
+
var GOOGLE_PUBLIC_KEYS_URL = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com";
|
|
733
|
+
var FIREBASE_APP_CHECK_AUDIENCE = "https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1.TokenExchangeService";
|
|
734
|
+
var MAX_CACHE_LAST_UPDATED_AT_SECONDS = 5 * 60;
|
|
735
|
+
var DEFAULT_CACHE_DURATION = 3600 * 1e3;
|
|
736
|
+
var CACHE_CONTROL_REGEX = /max-age=(\d+)/;
|
|
737
|
+
var TOKEN_EXPIRY_THRESHOLD_MILLIS = 5 * 60 * 1e3;
|
|
738
|
+
var GOOGLE_TOKEN_AUDIENCE = "https://accounts.google.com/o/oauth2/token";
|
|
739
|
+
var GOOGLE_AUTH_TOKEN_HOST = "accounts.google.com";
|
|
740
|
+
var GOOGLE_AUTH_TOKEN_PATH = "/o/oauth2/token";
|
|
741
|
+
var ONE_HOUR_IN_SECONDS = 60 * 60;
|
|
742
|
+
var ONE_MINUTE_IN_SECONDS = 60;
|
|
743
|
+
var ONE_MINUTE_IN_MILLIS = ONE_MINUTE_IN_SECONDS * 1e3;
|
|
744
|
+
var ONE_DAY_IN_MILLIS = 24 * 60 * 60 * 1e3;
|
|
745
|
+
var Attributes = {
|
|
746
|
+
AuthToken: "__ternsecureAuthToken",
|
|
747
|
+
AuthSignature: "__ternsecureAuthSignature",
|
|
748
|
+
AuthStatus: "__ternsecureAuthStatus",
|
|
749
|
+
AuthReason: "__ternsecureAuthReason",
|
|
750
|
+
AuthMessage: "__ternsecureAuthMessage",
|
|
751
|
+
TernSecureUrl: "__ternsecureUrl"
|
|
752
|
+
};
|
|
753
|
+
var Cookies = {
|
|
754
|
+
Session: "__session",
|
|
755
|
+
CsrfToken: "__terncf",
|
|
756
|
+
IdToken: "TernSecure_[DEFAULT]",
|
|
757
|
+
Refresh: "TernSecureID_[DEFAULT]",
|
|
758
|
+
Custom: "__custom",
|
|
759
|
+
TernAut: "tern_aut",
|
|
760
|
+
Handshake: "__ternsecure_handshake",
|
|
761
|
+
DevBrowser: "__ternsecure_db_jwt",
|
|
762
|
+
RedirectCount: "__ternsecure_redirect_count",
|
|
763
|
+
HandshakeNonce: "__ternsecure_handshake_nonce"
|
|
764
|
+
};
|
|
765
|
+
var QueryParameters = {
|
|
766
|
+
TernSynced: "__tern_synced",
|
|
767
|
+
SuffixedCookies: "suffixed_cookies",
|
|
768
|
+
TernRedirectUrl: "__tern_redirect_url",
|
|
769
|
+
// use the reference to Cookies to indicate that it's the same value
|
|
770
|
+
DevBrowser: Cookies.DevBrowser,
|
|
771
|
+
Handshake: Cookies.Handshake,
|
|
772
|
+
HandshakeHelp: "__tern_help",
|
|
773
|
+
LegacyDevBrowser: "__dev_session",
|
|
774
|
+
HandshakeReason: "__tern_hs_reason",
|
|
775
|
+
HandshakeNonce: Cookies.HandshakeNonce
|
|
776
|
+
};
|
|
777
|
+
var Headers2 = {
|
|
778
|
+
Accept: "accept",
|
|
779
|
+
AppCheckToken: "x-ternsecure-appcheck",
|
|
780
|
+
AuthMessage: "x-ternsecure-auth-message",
|
|
781
|
+
Authorization: "authorization",
|
|
782
|
+
AuthReason: "x-ternsecure-auth-reason",
|
|
783
|
+
AuthSignature: "x-ternsecure-auth-signature",
|
|
784
|
+
AuthStatus: "x-ternsecure-auth-status",
|
|
785
|
+
AuthToken: "x-ternsecure-auth-token",
|
|
786
|
+
CacheControl: "cache-control",
|
|
787
|
+
TernSecureRedirectTo: "x-ternsecure-redirect-to",
|
|
788
|
+
TernSecureRequestData: "x-ternsecure-request-data",
|
|
789
|
+
TernSecureUrl: "x-ternsecure-url",
|
|
790
|
+
CloudFrontForwardedProto: "cloudfront-forwarded-proto",
|
|
791
|
+
ContentType: "content-type",
|
|
792
|
+
ContentSecurityPolicy: "content-security-policy",
|
|
793
|
+
ContentSecurityPolicyReportOnly: "content-security-policy-report-only",
|
|
794
|
+
EnableDebug: "x-ternsecure-debug",
|
|
795
|
+
ForwardedHost: "x-forwarded-host",
|
|
796
|
+
ForwardedPort: "x-forwarded-port",
|
|
797
|
+
ForwardedProto: "x-forwarded-proto",
|
|
798
|
+
Host: "host",
|
|
799
|
+
Location: "location",
|
|
800
|
+
Nonce: "x-nonce",
|
|
801
|
+
Origin: "origin",
|
|
802
|
+
Referrer: "referer",
|
|
803
|
+
SecFetchDest: "sec-fetch-dest",
|
|
804
|
+
UserAgent: "user-agent",
|
|
805
|
+
ReportingEndpoints: "reporting-endpoints"
|
|
806
|
+
};
|
|
807
|
+
var ContentTypes = {
|
|
808
|
+
Json: "application/json"
|
|
809
|
+
};
|
|
810
|
+
var constants = {
|
|
811
|
+
Attributes,
|
|
812
|
+
Cookies,
|
|
813
|
+
Headers: Headers2,
|
|
814
|
+
ContentTypes,
|
|
815
|
+
QueryParameters
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// src/app-check/generator.ts
|
|
819
|
+
function transformMillisecondsToSecondsString(milliseconds) {
|
|
820
|
+
let duration;
|
|
821
|
+
const seconds = Math.floor(milliseconds / 1e3);
|
|
822
|
+
const nanos = Math.floor((milliseconds - seconds * 1e3) * 1e6);
|
|
823
|
+
if (nanos > 0) {
|
|
824
|
+
let nanoString = nanos.toString();
|
|
825
|
+
while (nanoString.length < 9) {
|
|
826
|
+
nanoString = "0" + nanoString;
|
|
827
|
+
}
|
|
828
|
+
duration = `${seconds}.${nanoString}s`;
|
|
829
|
+
} else {
|
|
830
|
+
duration = `${seconds}s`;
|
|
831
|
+
}
|
|
832
|
+
return duration;
|
|
833
|
+
}
|
|
834
|
+
var AppCheckTokenGenerator = class {
|
|
835
|
+
signer;
|
|
836
|
+
constructor(signer) {
|
|
837
|
+
this.signer = signer;
|
|
838
|
+
}
|
|
839
|
+
async createCustomToken(appId, options) {
|
|
840
|
+
if (!appId) {
|
|
841
|
+
throw new Error(
|
|
842
|
+
"appId is invalid"
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
let customOptions = {};
|
|
846
|
+
if (typeof options !== "undefined") {
|
|
847
|
+
customOptions = this.validateTokenOptions(options);
|
|
848
|
+
}
|
|
849
|
+
const account = await this.signer.getAccountId();
|
|
850
|
+
const iat = Math.floor(Date.now() / 1e3);
|
|
851
|
+
const body = {
|
|
852
|
+
iss: account,
|
|
853
|
+
sub: account,
|
|
854
|
+
app_id: appId,
|
|
855
|
+
aud: FIREBASE_APP_CHECK_AUDIENCE,
|
|
856
|
+
exp: iat + ONE_MINUTE_IN_SECONDS * 5,
|
|
857
|
+
iat,
|
|
858
|
+
...customOptions
|
|
859
|
+
};
|
|
860
|
+
return this.signer.sign(body);
|
|
861
|
+
}
|
|
862
|
+
validateTokenOptions(options) {
|
|
863
|
+
if (typeof options.ttlMillis !== "undefined") {
|
|
864
|
+
if (options.ttlMillis < ONE_MINUTE_IN_MILLIS * 30 || options.ttlMillis > ONE_DAY_IN_MILLIS * 7) {
|
|
865
|
+
throw new Error(
|
|
866
|
+
"ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive)."
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
return { ttl: transformMillisecondsToSecondsString(options.ttlMillis) };
|
|
870
|
+
}
|
|
871
|
+
return {};
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
// src/app-check/serverAppCheck.ts
|
|
876
|
+
var import_redis = require("@upstash/redis");
|
|
877
|
+
|
|
878
|
+
// src/admin/sessionTernSecure.ts
|
|
879
|
+
var import_errors4 = require("@tern-secure/shared/errors");
|
|
880
|
+
|
|
881
|
+
// src/utils/admin-init.ts
|
|
882
|
+
var import_firebase_admin = __toESM(require("firebase-admin"));
|
|
883
|
+
var import_app_check = require("firebase-admin/app-check");
|
|
884
|
+
|
|
885
|
+
// src/utils/config.ts
|
|
886
|
+
var loadAdminConfig = () => ({
|
|
887
|
+
projectId: process.env.FIREBASE_PROJECT_ID || "",
|
|
888
|
+
clientEmail: process.env.FIREBASE_CLIENT_EMAIL || "",
|
|
889
|
+
privateKey: process.env.FIREBASE_PRIVATE_KEY || ""
|
|
890
|
+
});
|
|
891
|
+
var validateAdminConfig = (config) => {
|
|
892
|
+
const requiredFields = [
|
|
893
|
+
"projectId",
|
|
894
|
+
"clientEmail",
|
|
895
|
+
"privateKey"
|
|
896
|
+
];
|
|
897
|
+
const errors = [];
|
|
898
|
+
requiredFields.forEach((field) => {
|
|
899
|
+
if (!config[field]) {
|
|
900
|
+
errors.push(`Missing required field: FIREBASE_${String(field).toUpperCase()}`);
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
return {
|
|
904
|
+
isValid: errors.length === 0,
|
|
905
|
+
errors,
|
|
906
|
+
config
|
|
907
|
+
};
|
|
908
|
+
};
|
|
909
|
+
var initializeAdminConfig = () => {
|
|
910
|
+
const config = loadAdminConfig();
|
|
911
|
+
const validationResult = validateAdminConfig(config);
|
|
912
|
+
if (!validationResult.isValid) {
|
|
913
|
+
throw new Error(
|
|
914
|
+
`Firebase Admin configuration validation failed:
|
|
915
|
+
${validationResult.errors.join("\n")}`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
return config;
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
// src/utils/admin-init.ts
|
|
922
|
+
if (!import_firebase_admin.default.apps.length) {
|
|
923
|
+
try {
|
|
924
|
+
const config = initializeAdminConfig();
|
|
925
|
+
import_firebase_admin.default.initializeApp({
|
|
926
|
+
credential: import_firebase_admin.default.credential.cert({
|
|
927
|
+
...config,
|
|
928
|
+
privateKey: config.privateKey.replace(/\\n/g, "\n")
|
|
929
|
+
})
|
|
930
|
+
});
|
|
931
|
+
} catch (error) {
|
|
932
|
+
console.error("Firebase admin initialization error", error);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
var adminTernSecureAuth = import_firebase_admin.default.auth();
|
|
936
|
+
var adminTernSecureDb = import_firebase_admin.default.firestore();
|
|
937
|
+
var TernSecureTenantManager = import_firebase_admin.default.auth().tenantManager();
|
|
938
|
+
var appCheckAdmin = (0, import_app_check.getAppCheck)();
|
|
939
|
+
|
|
940
|
+
// src/admin/sessionTernSecure.ts
|
|
941
|
+
var DEFAULT_COOKIE_CONFIG = {
|
|
942
|
+
DEFAULT_EXPIRES_IN_MS: 5 * 60 * 1e3,
|
|
943
|
+
// 5 minutes
|
|
944
|
+
DEFAULT_EXPIRES_IN_SECONDS: 5 * 60,
|
|
945
|
+
REVOKE_REFRESH_TOKENS_ON_SIGNOUT: true
|
|
946
|
+
};
|
|
947
|
+
var DEFAULT_COOKIE_OPTIONS = {
|
|
948
|
+
httpOnly: true,
|
|
949
|
+
secure: process.env.NODE_ENV === "production",
|
|
950
|
+
sameSite: "strict",
|
|
951
|
+
path: "/"
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
// src/admin/nextSessionTernSecure.ts
|
|
955
|
+
var import_cookie = require("@tern-secure/shared/cookie");
|
|
956
|
+
var import_errors5 = require("@tern-secure/shared/errors");
|
|
957
|
+
var import_headers = require("next/headers");
|
|
958
|
+
var SESSION_CONSTANTS = {
|
|
959
|
+
COOKIE_NAME: constants.Cookies.Session,
|
|
960
|
+
DEFAULT_EXPIRES_IN_MS: 60 * 60 * 24 * 5 * 1e3,
|
|
961
|
+
// 5 days
|
|
962
|
+
DEFAULT_EXPIRES_IN_SECONDS: 60 * 60 * 24 * 5,
|
|
963
|
+
REVOKE_REFRESH_TOKENS_ON_SIGNOUT: true
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
// src/tokens/ternSecureRequest.ts
|
|
967
|
+
var import_cookie2 = require("cookie");
|
|
968
|
+
|
|
969
|
+
// src/admin/user.ts
|
|
970
|
+
var import_errors6 = require("@tern-secure/shared/errors");
|
|
971
|
+
|
|
972
|
+
// src/app-check/verifier.ts
|
|
973
|
+
var import_jose6 = require("jose");
|
|
974
|
+
var getPublicKey = async (header, keyURL) => {
|
|
975
|
+
const jswksUrl = new URL(keyURL);
|
|
976
|
+
const getKey = (0, import_jose6.createRemoteJWKSet)(jswksUrl);
|
|
977
|
+
return getKey(header);
|
|
978
|
+
};
|
|
979
|
+
var verifyAppCheckToken = async (token, options) => {
|
|
980
|
+
const { data: decodedResult, errors } = ternDecodeJwt(token);
|
|
981
|
+
if (errors) {
|
|
982
|
+
throw errors[0];
|
|
983
|
+
}
|
|
984
|
+
const { header } = decodedResult;
|
|
985
|
+
const { kid } = header;
|
|
986
|
+
if (!kid) {
|
|
987
|
+
return {
|
|
988
|
+
errors: [
|
|
989
|
+
new TokenVerificationError({
|
|
990
|
+
reason: TokenVerificationErrorReason.TokenInvalid,
|
|
991
|
+
message: 'JWT "kid" header is missing.'
|
|
992
|
+
})
|
|
993
|
+
]
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
try {
|
|
997
|
+
const getPublicKeyForToken = () => getPublicKey(header, options.keyURL || "");
|
|
998
|
+
return await verifyAppCheckJwt(token, { ...options, key: getPublicKeyForToken });
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
if (error instanceof TokenVerificationError) {
|
|
1001
|
+
return { errors: [error] };
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
errors: [error]
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
var AppcheckTokenVerifier = class {
|
|
1009
|
+
constructor(credential) {
|
|
1010
|
+
this.credential = credential;
|
|
1011
|
+
}
|
|
1012
|
+
verifyToken = async (token, projectId, options) => {
|
|
1013
|
+
const { data, errors } = await verifyAppCheckToken(token, options);
|
|
1014
|
+
if (errors) {
|
|
1015
|
+
throw errors[0];
|
|
1016
|
+
}
|
|
1017
|
+
return data;
|
|
1018
|
+
};
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
// src/app-check/index.ts
|
|
1022
|
+
var JWKS_URL = "https://firebaseappcheck.googleapis.com/v1/jwks";
|
|
1023
|
+
var AppCheck = class {
|
|
1024
|
+
client;
|
|
1025
|
+
tokenGenerator;
|
|
1026
|
+
appCheckTokenVerifier;
|
|
1027
|
+
limitedUse;
|
|
1028
|
+
constructor(credential, tenantId, limitedUse) {
|
|
1029
|
+
this.client = new AppCheckApi(credential);
|
|
1030
|
+
this.tokenGenerator = new AppCheckTokenGenerator(
|
|
1031
|
+
cryptoSignerFromCredential(credential, tenantId)
|
|
1032
|
+
);
|
|
1033
|
+
this.appCheckTokenVerifier = new AppcheckTokenVerifier(credential);
|
|
1034
|
+
this.limitedUse = limitedUse;
|
|
1035
|
+
}
|
|
1036
|
+
createToken = (projectId, appId, options) => {
|
|
1037
|
+
return this.tokenGenerator.createCustomToken(appId, options).then((customToken) => {
|
|
1038
|
+
return this.client.exchangeToken({ customToken, projectId, appId });
|
|
1039
|
+
});
|
|
1040
|
+
};
|
|
1041
|
+
verifyToken = async (appCheckToken, projectId, options) => {
|
|
1042
|
+
return this.appCheckTokenVerifier.verifyToken(appCheckToken, projectId, { keyURL: JWKS_URL, ...options }).then((decodedToken) => {
|
|
1043
|
+
return {
|
|
1044
|
+
appId: decodedToken.app_id,
|
|
1045
|
+
token: decodedToken
|
|
1046
|
+
};
|
|
1047
|
+
});
|
|
1048
|
+
};
|
|
1049
|
+
};
|
|
1050
|
+
function getAppCheck2(serviceAccount, tenantId, limitedUse) {
|
|
1051
|
+
return new AppCheck(new ServiceAccountManager(serviceAccount), tenantId, limitedUse);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// src/tokens/keys.ts
|
|
1055
|
+
var cache = {};
|
|
1056
|
+
var lastUpdatedAt = 0;
|
|
1057
|
+
var googleExpiresAt = 0;
|
|
1058
|
+
function getFromCache(kid) {
|
|
1059
|
+
return cache[kid];
|
|
1060
|
+
}
|
|
1061
|
+
function getCacheValues() {
|
|
1062
|
+
return Object.values(cache);
|
|
1063
|
+
}
|
|
1064
|
+
function setInCache(kid, certificate, shouldExpire = true) {
|
|
1065
|
+
cache[kid] = certificate;
|
|
1066
|
+
lastUpdatedAt = shouldExpire ? Date.now() : -1;
|
|
1067
|
+
}
|
|
1068
|
+
async function fetchPublicKeys(keyUrl) {
|
|
1069
|
+
const url = new URL(keyUrl);
|
|
1070
|
+
const response = await fetch(url);
|
|
1071
|
+
if (!response.ok) {
|
|
1072
|
+
throw new TokenVerificationError({
|
|
1073
|
+
message: `Error loading public keys from ${url.href} with code=${response.status} `,
|
|
1074
|
+
reason: TokenVerificationErrorReason.TokenInvalid
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
const data = await response.json();
|
|
1078
|
+
const expiresAt = getExpiresAt(response);
|
|
1079
|
+
return {
|
|
1080
|
+
keys: data,
|
|
1081
|
+
expiresAt
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
async function loadJWKFromRemote({
|
|
1085
|
+
keyURL,
|
|
1086
|
+
skipJwksCache,
|
|
1087
|
+
kid
|
|
1088
|
+
}) {
|
|
1089
|
+
const finalKeyURL = keyURL || GOOGLE_PUBLIC_KEYS_URL;
|
|
1090
|
+
if (skipJwksCache || isCacheExpired() || !getFromCache(kid)) {
|
|
1091
|
+
const { keys, expiresAt } = await fetchPublicKeys(finalKeyURL);
|
|
1092
|
+
if (!keys || Object.keys(keys).length === 0) {
|
|
1093
|
+
throw new TokenVerificationError({
|
|
1094
|
+
message: `The JWKS endpoint ${finalKeyURL} returned no keys`,
|
|
1095
|
+
reason: TokenVerificationErrorReason.RemoteJWKFailedToLoad
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
googleExpiresAt = expiresAt;
|
|
1099
|
+
Object.entries(keys).forEach(([keyId, cert2]) => {
|
|
1100
|
+
setInCache(keyId, cert2);
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
const cert = getFromCache(kid);
|
|
1104
|
+
if (!cert) {
|
|
1105
|
+
getCacheValues();
|
|
1106
|
+
const availableKids = Object.keys(cache).sort().join(", ");
|
|
1107
|
+
throw new TokenVerificationError({
|
|
1108
|
+
message: `No public key found for kid "${kid}". Available kids: [${availableKids}]`,
|
|
608
1109
|
reason: TokenVerificationErrorReason.TokenInvalid
|
|
609
1110
|
});
|
|
610
1111
|
}
|
|
611
|
-
return
|
|
1112
|
+
return cert;
|
|
612
1113
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
// src/auth/constants.ts
|
|
618
|
-
var TOKEN_EXPIRY_THRESHOLD_MILLIS = 5 * 60 * 1e3;
|
|
619
|
-
var GOOGLE_TOKEN_AUDIENCE = "https://accounts.google.com/o/oauth2/token";
|
|
620
|
-
var GOOGLE_AUTH_TOKEN_HOST = "accounts.google.com";
|
|
621
|
-
var GOOGLE_AUTH_TOKEN_PATH = "/o/oauth2/token";
|
|
622
|
-
var ONE_HOUR_IN_SECONDS = 60 * 60;
|
|
623
|
-
|
|
624
|
-
// src/auth/utils.ts
|
|
625
|
-
async function getDetailFromResponse(response) {
|
|
626
|
-
const json = await response.json();
|
|
627
|
-
if (!json) {
|
|
628
|
-
return "Missing error payload";
|
|
1114
|
+
function isCacheExpired() {
|
|
1115
|
+
const now = Date.now();
|
|
1116
|
+
if (lastUpdatedAt === -1) {
|
|
1117
|
+
return false;
|
|
629
1118
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
1119
|
+
const cacheAge = now - lastUpdatedAt;
|
|
1120
|
+
const maxCacheAge = MAX_CACHE_LAST_UPDATED_AT_SECONDS * 1e3;
|
|
1121
|
+
const localCacheExpired = cacheAge >= maxCacheAge;
|
|
1122
|
+
const googleCacheExpired = now >= googleExpiresAt;
|
|
1123
|
+
const isExpired = localCacheExpired || googleCacheExpired;
|
|
1124
|
+
if (isExpired) {
|
|
1125
|
+
cache = {};
|
|
633
1126
|
}
|
|
634
|
-
return
|
|
635
|
-
}
|
|
636
|
-
async function fetchJson(url, init) {
|
|
637
|
-
return (await fetchAny(url, init)).json();
|
|
1127
|
+
return isExpired;
|
|
638
1128
|
}
|
|
639
|
-
|
|
640
|
-
const
|
|
641
|
-
if (!
|
|
642
|
-
|
|
1129
|
+
function getExpiresAt(res) {
|
|
1130
|
+
const cacheControlHeader = res.headers.get("cache-control");
|
|
1131
|
+
if (!cacheControlHeader) {
|
|
1132
|
+
return Date.now() + DEFAULT_CACHE_DURATION;
|
|
643
1133
|
}
|
|
644
|
-
|
|
1134
|
+
const maxAgeMatch = cacheControlHeader.match(CACHE_CONTROL_REGEX);
|
|
1135
|
+
const maxAge = maxAgeMatch ? parseInt(maxAgeMatch[1], 10) : DEFAULT_CACHE_DURATION / 1e3;
|
|
1136
|
+
return Date.now() + maxAge * 1e3;
|
|
645
1137
|
}
|
|
646
1138
|
|
|
647
|
-
// src/
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
throw new Error("Invalid access token response");
|
|
1139
|
+
// src/tokens/verify.ts
|
|
1140
|
+
async function verifyToken(token, options) {
|
|
1141
|
+
const { data: decodedResult, errors } = ternDecodeJwt(token);
|
|
1142
|
+
if (errors) {
|
|
1143
|
+
return { errors };
|
|
653
1144
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
this.privateKey = serviceAccount.privateKey;
|
|
666
|
-
this.clientEmail = serviceAccount.clientEmail;
|
|
1145
|
+
const { header } = decodedResult;
|
|
1146
|
+
const { kid } = header;
|
|
1147
|
+
if (!kid) {
|
|
1148
|
+
return {
|
|
1149
|
+
errors: [
|
|
1150
|
+
new TokenVerificationError({
|
|
1151
|
+
reason: TokenVerificationErrorReason.TokenInvalid,
|
|
1152
|
+
message: 'JWT "kid" header is missing.'
|
|
1153
|
+
})
|
|
1154
|
+
]
|
|
1155
|
+
};
|
|
667
1156
|
}
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
});
|
|
680
|
-
};
|
|
681
|
-
fetchAndCacheAccessToken = async (url) => {
|
|
682
|
-
const accessToken = await this.fetchAccessToken(url);
|
|
683
|
-
accessTokenCache.set(this.projectId, accessToken);
|
|
684
|
-
return accessToken;
|
|
685
|
-
};
|
|
686
|
-
getAccessToken = async (refresh) => {
|
|
687
|
-
const url = `https://${GOOGLE_AUTH_TOKEN_HOST}${GOOGLE_AUTH_TOKEN_PATH}`;
|
|
688
|
-
if (refresh) {
|
|
689
|
-
return this.fetchAndCacheAccessToken(url);
|
|
1157
|
+
try {
|
|
1158
|
+
const key = options.jwtKey || await loadJWKFromRemote({ ...options, kid });
|
|
1159
|
+
if (!key) {
|
|
1160
|
+
return {
|
|
1161
|
+
errors: [
|
|
1162
|
+
new TokenVerificationError({
|
|
1163
|
+
reason: TokenVerificationErrorReason.TokenInvalid,
|
|
1164
|
+
message: `No public key found for kid "${kid}".`
|
|
1165
|
+
})
|
|
1166
|
+
]
|
|
1167
|
+
};
|
|
690
1168
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1169
|
+
return await verifyJwt(token, { ...options, key });
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
if (error instanceof TokenVerificationError) {
|
|
1172
|
+
return { errors: [error] };
|
|
694
1173
|
}
|
|
695
|
-
return
|
|
696
|
-
|
|
697
|
-
createJwt = async () => {
|
|
698
|
-
const iat = Math.floor(Date.now() / 1e3);
|
|
699
|
-
const payload = {
|
|
700
|
-
aud: GOOGLE_TOKEN_AUDIENCE,
|
|
701
|
-
iat,
|
|
702
|
-
exp: iat + ONE_HOUR_IN_SECONDS,
|
|
703
|
-
iss: this.clientEmail,
|
|
704
|
-
sub: this.clientEmail,
|
|
705
|
-
scope: [
|
|
706
|
-
"https://www.googleapis.com/auth/cloud-platform",
|
|
707
|
-
"https://www.googleapis.com/auth/firebase.database",
|
|
708
|
-
"https://www.googleapis.com/auth/firebase.messaging",
|
|
709
|
-
"https://www.googleapis.com/auth/identitytoolkit",
|
|
710
|
-
"https://www.googleapis.com/auth/userinfo.email"
|
|
711
|
-
].join(" ")
|
|
1174
|
+
return {
|
|
1175
|
+
errors: [error]
|
|
712
1176
|
};
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
privateKey: this.privateKey
|
|
716
|
-
});
|
|
717
|
-
};
|
|
718
|
-
};
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
719
1179
|
|
|
720
1180
|
// src/auth/getauth.ts
|
|
721
1181
|
var API_KEY_ERROR = "API Key is required";
|
|
@@ -731,17 +1191,8 @@ function parseFirebaseResponse(data) {
|
|
|
731
1191
|
return data;
|
|
732
1192
|
}
|
|
733
1193
|
function getAuth(options) {
|
|
734
|
-
const { apiKey
|
|
735
|
-
const
|
|
736
|
-
const effectiveApiKey = apiKey || firebaseApiKey;
|
|
737
|
-
let credential = null;
|
|
738
|
-
if (firebaseAdminConfig?.projectId && firebaseAdminConfig?.privateKey && firebaseAdminConfig?.clientEmail) {
|
|
739
|
-
credential = new ServiceAccountTokenManager({
|
|
740
|
-
projectId: firebaseAdminConfig.projectId,
|
|
741
|
-
privateKey: firebaseAdminConfig.privateKey,
|
|
742
|
-
clientEmail: firebaseAdminConfig.clientEmail
|
|
743
|
-
});
|
|
744
|
-
}
|
|
1194
|
+
const { apiKey } = options;
|
|
1195
|
+
const effectiveApiKey = apiKey || process.env.NEXT_PUBLIC_FIREBASE_API_KEY;
|
|
745
1196
|
async function getUserData(idToken, localId) {
|
|
746
1197
|
if (!effectiveApiKey) {
|
|
747
1198
|
throw new Error(API_KEY_ERROR);
|
|
@@ -826,43 +1277,12 @@ function getAuth(options) {
|
|
|
826
1277
|
auth_time: decodedCustomIdToken.data.auth_time
|
|
827
1278
|
};
|
|
828
1279
|
}
|
|
829
|
-
async function
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
error: new Error(
|
|
834
|
-
"Firebase Admin config must be provided to exchange App Check tokens."
|
|
835
|
-
)
|
|
836
|
-
};
|
|
837
|
-
}
|
|
838
|
-
if (!effectiveApiKey) {
|
|
839
|
-
return { data: null, error: new Error(API_KEY_ERROR) };
|
|
840
|
-
}
|
|
1280
|
+
async function createAppCheckToken() {
|
|
1281
|
+
const adminConfig = loadAdminConfig();
|
|
1282
|
+
const appId = process.env.NEXT_PUBLIC_FIREBASE_APP_ID || "";
|
|
1283
|
+
const appCheck = getAppCheck2(adminConfig, options.tenantId);
|
|
841
1284
|
try {
|
|
842
|
-
const
|
|
843
|
-
if (decoded.errors) {
|
|
844
|
-
return { data: null, error: decoded.errors[0] };
|
|
845
|
-
}
|
|
846
|
-
const customToken = await createCustomToken(decoded.data.uid, {
|
|
847
|
-
emailVerified: decoded.data.email_verified,
|
|
848
|
-
source_sign_in_provider: decoded.data.firebase.sign_in_provider
|
|
849
|
-
});
|
|
850
|
-
const projectId = options.firebaseConfig?.projectId;
|
|
851
|
-
const appId = options.firebaseConfig?.appId;
|
|
852
|
-
if (!projectId || !appId) {
|
|
853
|
-
return { data: null, error: new Error("Project ID and App ID are required for App Check") };
|
|
854
|
-
}
|
|
855
|
-
const { accessToken } = await credential.getAccessToken();
|
|
856
|
-
const appCheckResponse = await options.apiClient?.appCheck.exchangeCustomToken({
|
|
857
|
-
accessToken,
|
|
858
|
-
projectId,
|
|
859
|
-
appId,
|
|
860
|
-
customToken,
|
|
861
|
-
limitedUse: false
|
|
862
|
-
});
|
|
863
|
-
if (!appCheckResponse?.token) {
|
|
864
|
-
return { data: null, error: new Error("Failed to exchange for App Check token") };
|
|
865
|
-
}
|
|
1285
|
+
const appCheckResponse = await appCheck.createToken(adminConfig.projectId, appId);
|
|
866
1286
|
return {
|
|
867
1287
|
data: {
|
|
868
1288
|
token: appCheckResponse.token,
|
|
@@ -874,16 +1294,104 @@ function getAuth(options) {
|
|
|
874
1294
|
return { data: null, error };
|
|
875
1295
|
}
|
|
876
1296
|
}
|
|
1297
|
+
async function verifyAppCheckToken2(token) {
|
|
1298
|
+
const adminConfig = loadAdminConfig();
|
|
1299
|
+
const appCheck = getAppCheck2(adminConfig, options.tenantId);
|
|
1300
|
+
try {
|
|
1301
|
+
const decodedToken = await appCheck.verifyToken(token, adminConfig.projectId, {});
|
|
1302
|
+
return {
|
|
1303
|
+
data: decodedToken,
|
|
1304
|
+
error: null
|
|
1305
|
+
};
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
return { data: null, error };
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
877
1310
|
return {
|
|
878
1311
|
getUserData,
|
|
879
1312
|
customForIdAndRefreshToken,
|
|
880
1313
|
createCustomIdAndRefreshToken,
|
|
881
1314
|
refreshExpiredIdToken,
|
|
882
|
-
|
|
1315
|
+
createAppCheckToken,
|
|
1316
|
+
verifyAppCheckToken: verifyAppCheckToken2
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// src/auth/credential.ts
|
|
1321
|
+
var accessTokenCache = /* @__PURE__ */ new Map();
|
|
1322
|
+
async function requestAccessToken(urlString, init) {
|
|
1323
|
+
const json = await fetchJson(urlString, init);
|
|
1324
|
+
if (!json.access_token || !json.expires_in) {
|
|
1325
|
+
throw new Error("Invalid access token response");
|
|
1326
|
+
}
|
|
1327
|
+
return {
|
|
1328
|
+
accessToken: json.access_token,
|
|
1329
|
+
expirationTime: Date.now() + json.expires_in * 1e3
|
|
883
1330
|
};
|
|
884
1331
|
}
|
|
1332
|
+
var ServiceAccountManager = class {
|
|
1333
|
+
projectId;
|
|
1334
|
+
privateKey;
|
|
1335
|
+
clientEmail;
|
|
1336
|
+
constructor(serviceAccount) {
|
|
1337
|
+
this.projectId = serviceAccount.projectId;
|
|
1338
|
+
this.privateKey = serviceAccount.privateKey;
|
|
1339
|
+
this.clientEmail = serviceAccount.clientEmail;
|
|
1340
|
+
}
|
|
1341
|
+
fetchAccessToken = async (url) => {
|
|
1342
|
+
const token = await this.createJwt();
|
|
1343
|
+
const postData = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" + token;
|
|
1344
|
+
return requestAccessToken(url, {
|
|
1345
|
+
method: "POST",
|
|
1346
|
+
headers: {
|
|
1347
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1348
|
+
Authorization: `Bearer ${token}`,
|
|
1349
|
+
Accept: "application/json"
|
|
1350
|
+
},
|
|
1351
|
+
body: postData
|
|
1352
|
+
});
|
|
1353
|
+
};
|
|
1354
|
+
fetchAndCacheAccessToken = async (url) => {
|
|
1355
|
+
const accessToken = await this.fetchAccessToken(url);
|
|
1356
|
+
accessTokenCache.set(this.projectId, accessToken);
|
|
1357
|
+
return accessToken;
|
|
1358
|
+
};
|
|
1359
|
+
getAccessToken = async (refresh) => {
|
|
1360
|
+
const url = `https://${GOOGLE_AUTH_TOKEN_HOST}${GOOGLE_AUTH_TOKEN_PATH}`;
|
|
1361
|
+
if (refresh) {
|
|
1362
|
+
return this.fetchAndCacheAccessToken(url);
|
|
1363
|
+
}
|
|
1364
|
+
const cachedResponse = accessTokenCache.get(this.projectId);
|
|
1365
|
+
if (!cachedResponse || cachedResponse.expirationTime - Date.now() <= TOKEN_EXPIRY_THRESHOLD_MILLIS) {
|
|
1366
|
+
return this.fetchAndCacheAccessToken(url);
|
|
1367
|
+
}
|
|
1368
|
+
return cachedResponse;
|
|
1369
|
+
};
|
|
1370
|
+
createJwt = async () => {
|
|
1371
|
+
const iat = Math.floor(Date.now() / 1e3);
|
|
1372
|
+
const payload = {
|
|
1373
|
+
aud: GOOGLE_TOKEN_AUDIENCE,
|
|
1374
|
+
iat,
|
|
1375
|
+
exp: iat + ONE_HOUR_IN_SECONDS,
|
|
1376
|
+
iss: this.clientEmail,
|
|
1377
|
+
sub: this.clientEmail,
|
|
1378
|
+
scope: [
|
|
1379
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
1380
|
+
"https://www.googleapis.com/auth/firebase.database",
|
|
1381
|
+
"https://www.googleapis.com/auth/firebase.messaging",
|
|
1382
|
+
"https://www.googleapis.com/auth/identitytoolkit",
|
|
1383
|
+
"https://www.googleapis.com/auth/userinfo.email"
|
|
1384
|
+
].join(" ")
|
|
1385
|
+
};
|
|
1386
|
+
return ternSignJwt({
|
|
1387
|
+
payload,
|
|
1388
|
+
privateKey: this.privateKey
|
|
1389
|
+
});
|
|
1390
|
+
};
|
|
1391
|
+
};
|
|
885
1392
|
// Annotate the CommonJS export names for ESM import in node:
|
|
886
1393
|
0 && (module.exports = {
|
|
1394
|
+
ServiceAccountManager,
|
|
887
1395
|
getAuth
|
|
888
1396
|
});
|
|
889
1397
|
//# sourceMappingURL=index.js.map
|