@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/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,6 +17,14 @@ 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/index.ts
|
|
@@ -28,6 +38,7 @@ __export(index_exports, {
|
|
|
28
38
|
createAdapter: () => createAdapter,
|
|
29
39
|
createBackendInstanceClient: () => createBackendInstanceClient,
|
|
30
40
|
createRedirect: () => createRedirect,
|
|
41
|
+
createRequestProcessor: () => createRequestProcessor,
|
|
31
42
|
createTernSecureRequest: () => createTernSecureRequest,
|
|
32
43
|
disableDebugLogging: () => disableDebugLogging,
|
|
33
44
|
enableDebugLogging: () => enableDebugLogging,
|
|
@@ -35,15 +46,25 @@ __export(index_exports, {
|
|
|
35
46
|
signedIn: () => signedIn,
|
|
36
47
|
signedInAuthObject: () => signedInAuthObject,
|
|
37
48
|
signedOutAuthObject: () => signedOutAuthObject,
|
|
38
|
-
validateCheckRevokedOptions: () => validateCheckRevokedOptions
|
|
49
|
+
validateCheckRevokedOptions: () => validateCheckRevokedOptions,
|
|
50
|
+
verifyToken: () => verifyToken
|
|
39
51
|
});
|
|
40
52
|
module.exports = __toCommonJS(index_exports);
|
|
41
53
|
|
|
42
54
|
// src/constants.ts
|
|
43
55
|
var GOOGLE_PUBLIC_KEYS_URL = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com";
|
|
56
|
+
var FIREBASE_APP_CHECK_AUDIENCE = "https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1.TokenExchangeService";
|
|
44
57
|
var MAX_CACHE_LAST_UPDATED_AT_SECONDS = 5 * 60;
|
|
45
58
|
var DEFAULT_CACHE_DURATION = 3600 * 1e3;
|
|
46
59
|
var CACHE_CONTROL_REGEX = /max-age=(\d+)/;
|
|
60
|
+
var TOKEN_EXPIRY_THRESHOLD_MILLIS = 5 * 60 * 1e3;
|
|
61
|
+
var GOOGLE_TOKEN_AUDIENCE = "https://accounts.google.com/o/oauth2/token";
|
|
62
|
+
var GOOGLE_AUTH_TOKEN_HOST = "accounts.google.com";
|
|
63
|
+
var GOOGLE_AUTH_TOKEN_PATH = "/o/oauth2/token";
|
|
64
|
+
var ONE_HOUR_IN_SECONDS = 60 * 60;
|
|
65
|
+
var ONE_MINUTE_IN_SECONDS = 60;
|
|
66
|
+
var ONE_MINUTE_IN_MILLIS = ONE_MINUTE_IN_SECONDS * 1e3;
|
|
67
|
+
var ONE_DAY_IN_MILLIS = 24 * 60 * 60 * 1e3;
|
|
47
68
|
var Attributes = {
|
|
48
69
|
AuthToken: "__ternsecureAuthToken",
|
|
49
70
|
AuthSignature: "__ternsecureAuthSignature",
|
|
@@ -78,7 +99,7 @@ var QueryParameters = {
|
|
|
78
99
|
};
|
|
79
100
|
var Headers2 = {
|
|
80
101
|
Accept: "accept",
|
|
81
|
-
AppCheckToken: "x-
|
|
102
|
+
AppCheckToken: "x-ternsecure-appcheck",
|
|
82
103
|
AuthMessage: "x-ternsecure-auth-message",
|
|
83
104
|
Authorization: "authorization",
|
|
84
105
|
AuthReason: "x-ternsecure-auth-reason",
|
|
@@ -238,12 +259,93 @@ var createTernSecureRequest = (...args) => {
|
|
|
238
259
|
return args[0] instanceof TernSecureRequest ? args[0] : new TernSecureRequest(...args);
|
|
239
260
|
};
|
|
240
261
|
|
|
262
|
+
// src/tokens/c-authenticateRequestProcessor.ts
|
|
263
|
+
var RequestProcessorContext = class {
|
|
264
|
+
constructor(ternSecureRequest, options) {
|
|
265
|
+
this.ternSecureRequest = ternSecureRequest;
|
|
266
|
+
this.options = options;
|
|
267
|
+
this.initHeaderValues();
|
|
268
|
+
this.initCookieValues();
|
|
269
|
+
this.initHandshakeValues();
|
|
270
|
+
this.initUrlValues();
|
|
271
|
+
Object.assign(this, options);
|
|
272
|
+
this.ternUrl = this.ternSecureRequest.ternUrl;
|
|
273
|
+
}
|
|
274
|
+
get request() {
|
|
275
|
+
return this.ternSecureRequest;
|
|
276
|
+
}
|
|
277
|
+
initHeaderValues() {
|
|
278
|
+
this.sessionTokenInHeader = this.parseAuthorizationHeader(
|
|
279
|
+
this.getHeader(constants.Headers.Authorization)
|
|
280
|
+
);
|
|
281
|
+
this.origin = this.getHeader(constants.Headers.Origin);
|
|
282
|
+
this.host = this.getHeader(constants.Headers.Host);
|
|
283
|
+
this.forwardedHost = this.getHeader(constants.Headers.ForwardedHost);
|
|
284
|
+
this.forwardedProto = this.getHeader(constants.Headers.CloudFrontForwardedProto) || this.getHeader(constants.Headers.ForwardedProto);
|
|
285
|
+
this.referrer = this.getHeader(constants.Headers.Referrer);
|
|
286
|
+
this.userAgent = this.getHeader(constants.Headers.UserAgent);
|
|
287
|
+
this.secFetchDest = this.getHeader(constants.Headers.SecFetchDest);
|
|
288
|
+
this.accept = this.getHeader(constants.Headers.Accept);
|
|
289
|
+
this.appCheckToken = this.getHeader(constants.Headers.AppCheckToken);
|
|
290
|
+
}
|
|
291
|
+
initCookieValues() {
|
|
292
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
293
|
+
const defaultPrefix = isProduction ? "__HOST-" : "__dev_";
|
|
294
|
+
this.sessionTokenInCookie = this.getCookie(constants.Cookies.Session);
|
|
295
|
+
this.idTokenInCookie = this.getCookie(`${defaultPrefix}${constants.Cookies.IdToken}`);
|
|
296
|
+
this.refreshTokenInCookie = this.getCookie(`${defaultPrefix}${constants.Cookies.Refresh}`);
|
|
297
|
+
this.csrfTokenInCookie = this.getCookie(constants.Cookies.CsrfToken);
|
|
298
|
+
this.customTokenInCookie = this.getCookie(constants.Cookies.Custom);
|
|
299
|
+
this.ternAuth = Number.parseInt(this.getCookie(constants.Cookies.TernAut) || "0", 10);
|
|
300
|
+
}
|
|
301
|
+
initHandshakeValues() {
|
|
302
|
+
this.handshakeToken = this.getQueryParam(constants.QueryParameters.Handshake) || this.getCookie(constants.Cookies.Handshake);
|
|
303
|
+
this.handshakeNonce = this.getQueryParam(constants.QueryParameters.HandshakeNonce) || this.getCookie(constants.Cookies.HandshakeNonce);
|
|
304
|
+
}
|
|
305
|
+
initUrlValues() {
|
|
306
|
+
this.method = this.ternSecureRequest.method;
|
|
307
|
+
this.pathSegments = this.ternSecureRequest.ternUrl.pathname.split("/").filter(Boolean);
|
|
308
|
+
this.endpoint = this.pathSegments[2];
|
|
309
|
+
this.subEndpoint = this.pathSegments[3];
|
|
310
|
+
}
|
|
311
|
+
getQueryParam(name) {
|
|
312
|
+
return this.ternSecureRequest.ternUrl.searchParams.get(name);
|
|
313
|
+
}
|
|
314
|
+
getHeader(name) {
|
|
315
|
+
return this.ternSecureRequest.headers.get(name) || void 0;
|
|
316
|
+
}
|
|
317
|
+
getCookie(name) {
|
|
318
|
+
return this.ternSecureRequest.cookies.get(name) || void 0;
|
|
319
|
+
}
|
|
320
|
+
parseAuthorizationHeader(authorizationHeader) {
|
|
321
|
+
if (!authorizationHeader) {
|
|
322
|
+
return void 0;
|
|
323
|
+
}
|
|
324
|
+
const [scheme, token] = authorizationHeader.split(" ", 2);
|
|
325
|
+
if (!token) {
|
|
326
|
+
return scheme;
|
|
327
|
+
}
|
|
328
|
+
if (scheme === "Bearer") {
|
|
329
|
+
return token;
|
|
330
|
+
}
|
|
331
|
+
return void 0;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
var createRequestProcessor = (ternSecureRequest, options) => {
|
|
335
|
+
return new RequestProcessorContext(ternSecureRequest, options);
|
|
336
|
+
};
|
|
337
|
+
|
|
241
338
|
// src/utils/mapDecode.ts
|
|
242
339
|
function mapJwtPayloadToDecodedIdToken(payload) {
|
|
243
340
|
const decodedIdToken = payload;
|
|
244
341
|
decodedIdToken.uid = decodedIdToken.sub;
|
|
245
342
|
return decodedIdToken;
|
|
246
343
|
}
|
|
344
|
+
function mapJwtPayloadToDecodedAppCheckToken(payload) {
|
|
345
|
+
const decodedAppCheckToken = payload;
|
|
346
|
+
decodedAppCheckToken.app_id = decodedAppCheckToken.sub;
|
|
347
|
+
return decodedAppCheckToken;
|
|
348
|
+
}
|
|
247
349
|
|
|
248
350
|
// src/tokens/authstate.ts
|
|
249
351
|
var AuthStatus = {
|
|
@@ -333,6 +435,7 @@ function signedOut(authCtx, reason, message = "", headers = new Headers()) {
|
|
|
333
435
|
isSignedIn: false,
|
|
334
436
|
auth: () => signedOutAuthObject(),
|
|
335
437
|
token: null,
|
|
438
|
+
appCheckToken: authCtx.appCheckToken,
|
|
336
439
|
headers
|
|
337
440
|
});
|
|
338
441
|
}
|
|
@@ -356,106 +459,596 @@ var decorateHeaders = (requestState) => {
|
|
|
356
459
|
} catch {
|
|
357
460
|
}
|
|
358
461
|
}
|
|
462
|
+
if (requestState.appCheckToken) {
|
|
463
|
+
try {
|
|
464
|
+
headers.set(constants.Headers.AppCheckToken, requestState.appCheckToken);
|
|
465
|
+
} catch {
|
|
466
|
+
}
|
|
467
|
+
}
|
|
359
468
|
requestState.headers = headers;
|
|
360
469
|
return requestState;
|
|
361
470
|
};
|
|
362
471
|
|
|
363
|
-
// src/
|
|
364
|
-
var
|
|
365
|
-
|
|
366
|
-
|
|
472
|
+
// src/jwt/verifyJwt.ts
|
|
473
|
+
var import_jose2 = require("jose");
|
|
474
|
+
|
|
475
|
+
// src/utils/errors.ts
|
|
476
|
+
var RefreshTokenErrorReason = {
|
|
477
|
+
NonEligibleNoCookie: "non-eligible-no-refresh-cookie",
|
|
478
|
+
NonEligibleNonGet: "non-eligible-non-get",
|
|
479
|
+
InvalidSessionToken: "invalid-session-token",
|
|
480
|
+
MissingApiClient: "missing-api-client",
|
|
481
|
+
MissingIdToken: "missing-id-token",
|
|
482
|
+
MissingSessionToken: "missing-session-token",
|
|
483
|
+
MissingRefreshToken: "missing-refresh-token",
|
|
484
|
+
ExpiredIdTokenDecodeFailed: "expired-id-token-decode-failed",
|
|
485
|
+
ExpiredSessionTokenDecodeFailed: "expired-session-token-decode-failed",
|
|
486
|
+
FetchError: "fetch-error"
|
|
487
|
+
};
|
|
488
|
+
var TokenVerificationErrorReason = {
|
|
489
|
+
TokenExpired: "token-expired",
|
|
490
|
+
TokenInvalid: "token-invalid",
|
|
491
|
+
TokenInvalidAlgorithm: "token-invalid-algorithm",
|
|
492
|
+
TokenInvalidAuthorizedParties: "token-invalid-authorized-parties",
|
|
493
|
+
TokenInvalidSignature: "token-invalid-signature",
|
|
494
|
+
TokenNotActiveYet: "token-not-active-yet",
|
|
495
|
+
TokenIatInTheFuture: "token-iat-in-the-future",
|
|
496
|
+
TokenVerificationFailed: "token-verification-failed",
|
|
497
|
+
InvalidSecretKey: "secret-key-invalid",
|
|
498
|
+
LocalJWKMissing: "jwk-local-missing",
|
|
499
|
+
RemoteJWKFailedToLoad: "jwk-remote-failed-to-load",
|
|
500
|
+
RemoteJWKInvalid: "jwk-remote-invalid",
|
|
501
|
+
RemoteJWKMissing: "jwk-remote-missing",
|
|
502
|
+
JWKFailedToResolve: "jwk-failed-to-resolve",
|
|
503
|
+
JWKKidMismatch: "jwk-kid-mismatch"
|
|
504
|
+
};
|
|
505
|
+
var TokenVerificationError = class _TokenVerificationError extends Error {
|
|
506
|
+
reason;
|
|
507
|
+
tokenCarrier;
|
|
508
|
+
constructor({
|
|
509
|
+
message,
|
|
510
|
+
reason
|
|
511
|
+
}) {
|
|
512
|
+
super(message);
|
|
513
|
+
Object.setPrototypeOf(this, _TokenVerificationError.prototype);
|
|
514
|
+
this.reason = reason;
|
|
515
|
+
this.message = message;
|
|
367
516
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
throw new Error("A valid API key is required.");
|
|
371
|
-
}
|
|
517
|
+
getFullMessage() {
|
|
518
|
+
return `${[this.message].filter((m) => m).join(" ")} (reason=${this.reason}, token-carrier=${this.tokenCarrier})`;
|
|
372
519
|
}
|
|
373
520
|
};
|
|
374
521
|
|
|
375
|
-
// src/
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
522
|
+
// src/utils/rfc4648.ts
|
|
523
|
+
var base64url = {
|
|
524
|
+
parse(string, opts) {
|
|
525
|
+
return parse2(string, base64UrlEncoding, opts);
|
|
526
|
+
},
|
|
527
|
+
stringify(data, opts) {
|
|
528
|
+
return stringify(data, base64UrlEncoding, opts);
|
|
529
|
+
}
|
|
381
530
|
};
|
|
382
|
-
var
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
531
|
+
var base64UrlEncoding = {
|
|
532
|
+
chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
|
|
533
|
+
bits: 6
|
|
534
|
+
};
|
|
535
|
+
function parse2(string, encoding, opts = {}) {
|
|
536
|
+
if (!encoding.codes) {
|
|
537
|
+
encoding.codes = {};
|
|
538
|
+
for (let i = 0; i < encoding.chars.length; ++i) {
|
|
539
|
+
encoding.codes[encoding.chars[i]] = i;
|
|
387
540
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
try {
|
|
398
|
-
const response = await fetch(endpoint, {
|
|
399
|
-
method: "POST",
|
|
400
|
-
headers,
|
|
401
|
-
body: JSON.stringify(body)
|
|
402
|
-
});
|
|
403
|
-
if (!response.ok) {
|
|
404
|
-
const errorText = await response.text();
|
|
405
|
-
throw new Error(`App Check token exchange failed: ${response.status} ${errorText}`);
|
|
406
|
-
}
|
|
407
|
-
const data = await response.json();
|
|
408
|
-
return {
|
|
409
|
-
token: data.token,
|
|
410
|
-
ttl: data.ttl
|
|
411
|
-
};
|
|
412
|
-
} catch (error) {
|
|
413
|
-
console.warn("[ternsecure - appcheck api]unexpected error:", error);
|
|
414
|
-
throw error;
|
|
541
|
+
}
|
|
542
|
+
if (!opts.loose && string.length * encoding.bits & 7) {
|
|
543
|
+
throw new SyntaxError("Invalid padding");
|
|
544
|
+
}
|
|
545
|
+
let end = string.length;
|
|
546
|
+
while (string[end - 1] === "=") {
|
|
547
|
+
--end;
|
|
548
|
+
if (!opts.loose && !((string.length - end) * encoding.bits & 7)) {
|
|
549
|
+
throw new SyntaxError("Invalid padding");
|
|
415
550
|
}
|
|
416
551
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
552
|
+
const out = new (opts.out ?? Uint8Array)(end * encoding.bits / 8 | 0);
|
|
553
|
+
let bits = 0;
|
|
554
|
+
let buffer = 0;
|
|
555
|
+
let written = 0;
|
|
556
|
+
for (let i = 0; i < end; ++i) {
|
|
557
|
+
const value = encoding.codes[string[i]];
|
|
558
|
+
if (value === void 0) {
|
|
559
|
+
throw new SyntaxError("Invalid character " + string[i]);
|
|
421
560
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const body = {
|
|
428
|
-
customToken,
|
|
429
|
-
limitedUse
|
|
430
|
-
};
|
|
431
|
-
try {
|
|
432
|
-
const response = await fetch(endpoint, {
|
|
433
|
-
method: "POST",
|
|
434
|
-
headers,
|
|
435
|
-
body: JSON.stringify(body)
|
|
436
|
-
});
|
|
437
|
-
if (!response.ok) {
|
|
438
|
-
const errorText = await response.text();
|
|
439
|
-
throw new Error(`App Check token exchange failed: ${response.status} ${errorText}`);
|
|
440
|
-
}
|
|
441
|
-
const data = await response.json();
|
|
442
|
-
return {
|
|
443
|
-
token: data.token,
|
|
444
|
-
ttl: data.ttl
|
|
445
|
-
};
|
|
446
|
-
} catch (error) {
|
|
447
|
-
console.warn("[ternsecure - appcheck api]unexpected error:", error);
|
|
448
|
-
throw error;
|
|
561
|
+
buffer = buffer << encoding.bits | value;
|
|
562
|
+
bits += encoding.bits;
|
|
563
|
+
if (bits >= 8) {
|
|
564
|
+
bits -= 8;
|
|
565
|
+
out[written++] = 255 & buffer >> bits;
|
|
449
566
|
}
|
|
450
567
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
568
|
+
if (bits >= encoding.bits || 255 & buffer << 8 - bits) {
|
|
569
|
+
throw new SyntaxError("Unexpected end of data");
|
|
570
|
+
}
|
|
571
|
+
return out;
|
|
572
|
+
}
|
|
573
|
+
function stringify(data, encoding, opts = {}) {
|
|
574
|
+
const { pad = true } = opts;
|
|
575
|
+
const mask = (1 << encoding.bits) - 1;
|
|
576
|
+
let out = "";
|
|
577
|
+
let bits = 0;
|
|
578
|
+
let buffer = 0;
|
|
579
|
+
for (let i = 0; i < data.length; ++i) {
|
|
580
|
+
buffer = buffer << 8 | 255 & data[i];
|
|
581
|
+
bits += 8;
|
|
582
|
+
while (bits > encoding.bits) {
|
|
583
|
+
bits -= encoding.bits;
|
|
584
|
+
out += encoding.chars[mask & buffer >> bits];
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (bits) {
|
|
588
|
+
out += encoding.chars[mask & buffer << encoding.bits - bits];
|
|
589
|
+
}
|
|
590
|
+
if (pad) {
|
|
591
|
+
while (out.length * encoding.bits & 7) {
|
|
592
|
+
out += "=";
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return out;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/jwt/cryptoKeys.ts
|
|
599
|
+
var import_jose = require("jose");
|
|
600
|
+
async function importKey(key, algorithm) {
|
|
601
|
+
if (typeof key === "object") {
|
|
602
|
+
const result = await (0, import_jose.importJWK)(key, algorithm);
|
|
603
|
+
if (result instanceof Uint8Array) {
|
|
604
|
+
throw new Error("Unexpected Uint8Array result from JWK import");
|
|
605
|
+
}
|
|
606
|
+
return result;
|
|
607
|
+
}
|
|
608
|
+
const keyString = key.trim();
|
|
609
|
+
if (keyString.includes("-----BEGIN CERTIFICATE-----")) {
|
|
610
|
+
return await (0, import_jose.importX509)(keyString, algorithm);
|
|
611
|
+
}
|
|
612
|
+
if (keyString.includes("-----BEGIN PUBLIC KEY-----")) {
|
|
613
|
+
return await (0, import_jose.importSPKI)(keyString, algorithm);
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
return await (0, import_jose.importSPKI)(keyString, algorithm);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
`Unsupported key format. Supported formats: X.509 certificate (PEM), SPKI (PEM), JWK (JSON object or string). Error: ${error}`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/jwt/algorithms.ts
|
|
625
|
+
var algToHash = {
|
|
626
|
+
RS256: "SHA-256",
|
|
627
|
+
RS384: "SHA-384",
|
|
628
|
+
RS512: "SHA-512"
|
|
629
|
+
};
|
|
630
|
+
var algs = Object.keys(algToHash);
|
|
631
|
+
|
|
632
|
+
// src/jwt/verifyContent.ts
|
|
633
|
+
var verifyHeaderKid = (kid) => {
|
|
634
|
+
if (typeof kid === "undefined") {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (typeof kid !== "string") {
|
|
638
|
+
throw new TokenVerificationError({
|
|
639
|
+
reason: TokenVerificationErrorReason.TokenInvalid,
|
|
640
|
+
message: `Invalid JWT kid ${JSON.stringify(kid)}. Expected a string.`
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
var verifySubClaim = (sub) => {
|
|
645
|
+
if (typeof sub !== "string") {
|
|
646
|
+
throw new TokenVerificationError({
|
|
647
|
+
reason: TokenVerificationErrorReason.TokenVerificationFailed,
|
|
648
|
+
message: `Subject claim (sub) is required and must be a string. Received ${JSON.stringify(sub)}.`
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
var verifyExpirationClaim = (exp, clockSkewInMs) => {
|
|
653
|
+
if (typeof exp !== "number") {
|
|
654
|
+
throw new TokenVerificationError({
|
|
655
|
+
reason: TokenVerificationErrorReason.TokenVerificationFailed,
|
|
656
|
+
message: `Invalid JWT expiry date (exp) claim ${JSON.stringify(exp)}. Expected a number.`
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
const currentDate = new Date(Date.now());
|
|
660
|
+
const expiryDate = /* @__PURE__ */ new Date(0);
|
|
661
|
+
expiryDate.setUTCSeconds(exp);
|
|
662
|
+
const expired = expiryDate.getTime() <= currentDate.getTime() - clockSkewInMs;
|
|
663
|
+
if (expired) {
|
|
664
|
+
throw new TokenVerificationError({
|
|
665
|
+
reason: TokenVerificationErrorReason.TokenExpired,
|
|
666
|
+
message: `JWT is expired. Expiry date: ${expiryDate.toUTCString()}, Current date: ${currentDate.toUTCString()}.`
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
var verifyIssuedAtClaim = (iat, clockSkewInMs) => {
|
|
671
|
+
if (typeof iat === "undefined") {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (typeof iat !== "number") {
|
|
675
|
+
throw new TokenVerificationError({
|
|
676
|
+
reason: TokenVerificationErrorReason.TokenVerificationFailed,
|
|
677
|
+
message: `Invalid JWT issued at date claim (iat) ${JSON.stringify(iat)}. Expected a number.`
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
const currentDate = new Date(Date.now());
|
|
681
|
+
const issuedAtDate = /* @__PURE__ */ new Date(0);
|
|
682
|
+
issuedAtDate.setUTCSeconds(iat);
|
|
683
|
+
const postIssued = issuedAtDate.getTime() > currentDate.getTime() + clockSkewInMs;
|
|
684
|
+
if (postIssued) {
|
|
685
|
+
throw new TokenVerificationError({
|
|
686
|
+
reason: TokenVerificationErrorReason.TokenIatInTheFuture,
|
|
687
|
+
message: `JWT issued at date claim (iat) is in the future. Issued at date: ${issuedAtDate.toUTCString()}; Current date: ${currentDate.toUTCString()};`
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
// src/jwt/verifyJwt.ts
|
|
693
|
+
var DEFAULT_CLOCK_SKEW_IN_MS = 5 * 1e3;
|
|
694
|
+
async function verifySignature(jwt, key) {
|
|
695
|
+
const { header, raw } = jwt;
|
|
696
|
+
const joseAlgorithm = header.alg || "RS256";
|
|
697
|
+
try {
|
|
698
|
+
const publicKey = await importKey(key, joseAlgorithm);
|
|
699
|
+
const { payload } = await (0, import_jose2.jwtVerify)(raw.text, publicKey);
|
|
700
|
+
return { data: payload };
|
|
701
|
+
} catch (error) {
|
|
702
|
+
return {
|
|
703
|
+
errors: [
|
|
704
|
+
new TokenVerificationError({
|
|
705
|
+
reason: TokenVerificationErrorReason.TokenInvalidSignature,
|
|
706
|
+
message: error.message
|
|
707
|
+
})
|
|
708
|
+
]
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function ternDecodeJwt(token) {
|
|
713
|
+
try {
|
|
714
|
+
const header = (0, import_jose2.decodeProtectedHeader)(token);
|
|
715
|
+
const payload = (0, import_jose2.decodeJwt)(token);
|
|
716
|
+
const tokenParts = (token || "").toString().split(".");
|
|
717
|
+
if (tokenParts.length !== 3) {
|
|
718
|
+
return {
|
|
719
|
+
errors: [
|
|
720
|
+
new TokenVerificationError({
|
|
721
|
+
reason: TokenVerificationErrorReason.TokenInvalid,
|
|
722
|
+
message: "Invalid JWT format"
|
|
723
|
+
})
|
|
724
|
+
]
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
const [rawHeader, rawPayload, rawSignature] = tokenParts;
|
|
728
|
+
const signature = base64url.parse(rawSignature, { loose: true });
|
|
729
|
+
const data = {
|
|
730
|
+
header,
|
|
731
|
+
payload,
|
|
732
|
+
signature,
|
|
733
|
+
raw: {
|
|
734
|
+
header: rawHeader,
|
|
735
|
+
payload: rawPayload,
|
|
736
|
+
signature: rawSignature,
|
|
737
|
+
text: token
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
return { data };
|
|
741
|
+
} catch (error) {
|
|
742
|
+
return {
|
|
743
|
+
errors: [
|
|
744
|
+
new TokenVerificationError({
|
|
745
|
+
reason: TokenVerificationErrorReason.TokenInvalid,
|
|
746
|
+
message: `${error.message || "Invalid Token or Protected Header formatting"} (Token length: ${token?.length}, First 10 chars: ${token?.substring(0, 10)}...)`
|
|
747
|
+
})
|
|
748
|
+
]
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
async function verifyJwt(token, options) {
|
|
753
|
+
const { key } = options;
|
|
754
|
+
const clockSkew = options.clockSkewInMs || DEFAULT_CLOCK_SKEW_IN_MS;
|
|
755
|
+
const { data: decoded, errors } = ternDecodeJwt(token);
|
|
756
|
+
if (errors) {
|
|
757
|
+
return { errors };
|
|
758
|
+
}
|
|
759
|
+
const { header, payload } = decoded;
|
|
760
|
+
try {
|
|
761
|
+
verifyHeaderKid(header.kid);
|
|
762
|
+
verifySubClaim(payload.sub);
|
|
763
|
+
verifyExpirationClaim(payload.exp, clockSkew);
|
|
764
|
+
verifyIssuedAtClaim(payload.iat, clockSkew);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
return { errors: [error] };
|
|
767
|
+
}
|
|
768
|
+
const { data: verifiedPayload, errors: signatureErrors } = await verifySignature(decoded, key);
|
|
769
|
+
if (signatureErrors) {
|
|
770
|
+
return {
|
|
771
|
+
errors: [
|
|
772
|
+
new TokenVerificationError({
|
|
773
|
+
reason: TokenVerificationErrorReason.TokenInvalidSignature,
|
|
774
|
+
message: "Token signature verification failed."
|
|
775
|
+
})
|
|
776
|
+
]
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
const decodedIdToken = mapJwtPayloadToDecodedIdToken(verifiedPayload);
|
|
780
|
+
return { data: decodedIdToken };
|
|
781
|
+
}
|
|
782
|
+
async function verifyAppCheckSignature(jwt, getPublicKey2) {
|
|
783
|
+
const { header, raw } = jwt;
|
|
784
|
+
const joseAlgorithm = header.alg || "RS256";
|
|
785
|
+
try {
|
|
786
|
+
const key = await getPublicKey2();
|
|
787
|
+
const { payload } = await (0, import_jose2.jwtVerify)(raw.text, key);
|
|
788
|
+
return { data: payload };
|
|
789
|
+
} catch (error) {
|
|
790
|
+
return {
|
|
791
|
+
errors: [
|
|
792
|
+
new TokenVerificationError({
|
|
793
|
+
reason: TokenVerificationErrorReason.TokenInvalidSignature,
|
|
794
|
+
message: error.message
|
|
795
|
+
})
|
|
796
|
+
]
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
async function verifyAppCheckJwt(token, options) {
|
|
801
|
+
const { key: getPublicKey2 } = options;
|
|
802
|
+
const clockSkew = options.clockSkewInMs || DEFAULT_CLOCK_SKEW_IN_MS;
|
|
803
|
+
const { data: decoded, errors } = ternDecodeJwt(token);
|
|
804
|
+
if (errors) {
|
|
805
|
+
return { errors };
|
|
806
|
+
}
|
|
807
|
+
const { header, payload } = decoded;
|
|
808
|
+
try {
|
|
809
|
+
verifyHeaderKid(header.kid);
|
|
810
|
+
verifySubClaim(payload.sub);
|
|
811
|
+
verifyExpirationClaim(payload.exp, clockSkew);
|
|
812
|
+
verifyIssuedAtClaim(payload.iat, clockSkew);
|
|
813
|
+
} catch (error) {
|
|
814
|
+
return { errors: [error] };
|
|
815
|
+
}
|
|
816
|
+
const { data: verifiedPayload, errors: signatureErrors } = await verifyAppCheckSignature(
|
|
817
|
+
decoded,
|
|
818
|
+
getPublicKey2
|
|
819
|
+
);
|
|
820
|
+
if (signatureErrors) {
|
|
821
|
+
return {
|
|
822
|
+
errors: [
|
|
823
|
+
new TokenVerificationError({
|
|
824
|
+
reason: TokenVerificationErrorReason.TokenInvalidSignature,
|
|
825
|
+
message: "Token signature verification failed."
|
|
826
|
+
})
|
|
827
|
+
]
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
const decodedAppCheckToken = mapJwtPayloadToDecodedAppCheckToken(verifiedPayload);
|
|
831
|
+
return { data: decodedAppCheckToken };
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/tokens/keys.ts
|
|
835
|
+
var cache = {};
|
|
836
|
+
var lastUpdatedAt = 0;
|
|
837
|
+
var googleExpiresAt = 0;
|
|
838
|
+
function getFromCache(kid) {
|
|
839
|
+
return cache[kid];
|
|
840
|
+
}
|
|
841
|
+
function getCacheValues() {
|
|
842
|
+
return Object.values(cache);
|
|
843
|
+
}
|
|
844
|
+
function setInCache(kid, certificate, shouldExpire = true) {
|
|
845
|
+
cache[kid] = certificate;
|
|
846
|
+
lastUpdatedAt = shouldExpire ? Date.now() : -1;
|
|
847
|
+
}
|
|
848
|
+
async function fetchPublicKeys(keyUrl) {
|
|
849
|
+
const url = new URL(keyUrl);
|
|
850
|
+
const response = await fetch(url);
|
|
851
|
+
if (!response.ok) {
|
|
852
|
+
throw new TokenVerificationError({
|
|
853
|
+
message: `Error loading public keys from ${url.href} with code=${response.status} `,
|
|
854
|
+
reason: TokenVerificationErrorReason.TokenInvalid
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
const data = await response.json();
|
|
858
|
+
const expiresAt = getExpiresAt(response);
|
|
859
|
+
return {
|
|
860
|
+
keys: data,
|
|
861
|
+
expiresAt
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
async function loadJWKFromRemote({
|
|
865
|
+
keyURL,
|
|
866
|
+
skipJwksCache,
|
|
867
|
+
kid
|
|
868
|
+
}) {
|
|
869
|
+
const finalKeyURL = keyURL || GOOGLE_PUBLIC_KEYS_URL;
|
|
870
|
+
if (skipJwksCache || isCacheExpired() || !getFromCache(kid)) {
|
|
871
|
+
const { keys, expiresAt } = await fetchPublicKeys(finalKeyURL);
|
|
872
|
+
if (!keys || Object.keys(keys).length === 0) {
|
|
873
|
+
throw new TokenVerificationError({
|
|
874
|
+
message: `The JWKS endpoint ${finalKeyURL} returned no keys`,
|
|
875
|
+
reason: TokenVerificationErrorReason.RemoteJWKFailedToLoad
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
googleExpiresAt = expiresAt;
|
|
879
|
+
Object.entries(keys).forEach(([keyId, cert2]) => {
|
|
880
|
+
setInCache(keyId, cert2);
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
const cert = getFromCache(kid);
|
|
884
|
+
if (!cert) {
|
|
885
|
+
getCacheValues();
|
|
886
|
+
const availableKids = Object.keys(cache).sort().join(", ");
|
|
887
|
+
throw new TokenVerificationError({
|
|
888
|
+
message: `No public key found for kid "${kid}". Available kids: [${availableKids}]`,
|
|
889
|
+
reason: TokenVerificationErrorReason.TokenInvalid
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
return cert;
|
|
893
|
+
}
|
|
894
|
+
function isCacheExpired() {
|
|
895
|
+
const now = Date.now();
|
|
896
|
+
if (lastUpdatedAt === -1) {
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
const cacheAge = now - lastUpdatedAt;
|
|
900
|
+
const maxCacheAge = MAX_CACHE_LAST_UPDATED_AT_SECONDS * 1e3;
|
|
901
|
+
const localCacheExpired = cacheAge >= maxCacheAge;
|
|
902
|
+
const googleCacheExpired = now >= googleExpiresAt;
|
|
903
|
+
const isExpired = localCacheExpired || googleCacheExpired;
|
|
904
|
+
if (isExpired) {
|
|
905
|
+
cache = {};
|
|
906
|
+
}
|
|
907
|
+
return isExpired;
|
|
908
|
+
}
|
|
909
|
+
function getExpiresAt(res) {
|
|
910
|
+
const cacheControlHeader = res.headers.get("cache-control");
|
|
911
|
+
if (!cacheControlHeader) {
|
|
912
|
+
return Date.now() + DEFAULT_CACHE_DURATION;
|
|
913
|
+
}
|
|
914
|
+
const maxAgeMatch = cacheControlHeader.match(CACHE_CONTROL_REGEX);
|
|
915
|
+
const maxAge = maxAgeMatch ? parseInt(maxAgeMatch[1], 10) : DEFAULT_CACHE_DURATION / 1e3;
|
|
916
|
+
return Date.now() + maxAge * 1e3;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// src/tokens/verify.ts
|
|
920
|
+
async function verifyToken(token, options) {
|
|
921
|
+
const { data: decodedResult, errors } = ternDecodeJwt(token);
|
|
922
|
+
if (errors) {
|
|
923
|
+
return { errors };
|
|
924
|
+
}
|
|
925
|
+
const { header } = decodedResult;
|
|
926
|
+
const { kid } = header;
|
|
927
|
+
if (!kid) {
|
|
928
|
+
return {
|
|
929
|
+
errors: [
|
|
930
|
+
new TokenVerificationError({
|
|
931
|
+
reason: TokenVerificationErrorReason.TokenInvalid,
|
|
932
|
+
message: 'JWT "kid" header is missing.'
|
|
933
|
+
})
|
|
934
|
+
]
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
try {
|
|
938
|
+
const key = options.jwtKey || await loadJWKFromRemote({ ...options, kid });
|
|
939
|
+
if (!key) {
|
|
940
|
+
return {
|
|
941
|
+
errors: [
|
|
942
|
+
new TokenVerificationError({
|
|
943
|
+
reason: TokenVerificationErrorReason.TokenInvalid,
|
|
944
|
+
message: `No public key found for kid "${kid}".`
|
|
945
|
+
})
|
|
946
|
+
]
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
return await verifyJwt(token, { ...options, key });
|
|
950
|
+
} catch (error) {
|
|
951
|
+
if (error instanceof TokenVerificationError) {
|
|
952
|
+
return { errors: [error] };
|
|
953
|
+
}
|
|
954
|
+
return {
|
|
955
|
+
errors: [error]
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/fireRestApi/endpoints/AbstractApi.ts
|
|
961
|
+
var AbstractAPI = class {
|
|
962
|
+
constructor(request) {
|
|
963
|
+
this.request = request;
|
|
964
|
+
}
|
|
965
|
+
requireApiKey(apiKey) {
|
|
966
|
+
if (!apiKey) {
|
|
967
|
+
throw new Error("A valid API key is required.");
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
// src/fireRestApi/endpoints/AppCheckApi.ts
|
|
973
|
+
function getSdkVersion() {
|
|
974
|
+
return "12.7.0";
|
|
975
|
+
}
|
|
976
|
+
var FIREBASE_APP_CHECK_CONFIG_HEADERS = {
|
|
977
|
+
"X-Firebase-Client": `fire-admin-node/${getSdkVersion()}`
|
|
978
|
+
};
|
|
979
|
+
var AppCheckApi = class extends AbstractAPI {
|
|
980
|
+
async exchangeCustomToken(params) {
|
|
981
|
+
const { projectId, appId, customToken, accessToken, limitedUse = false } = params;
|
|
982
|
+
if (!projectId || !appId) {
|
|
983
|
+
throw new Error("Project ID and App ID are required for App Check token exchange");
|
|
984
|
+
}
|
|
985
|
+
const endpoint = `https://firebaseappcheck.googleapis.com/v1beta/projects/${projectId}/apps/${appId}:exchangeCustomToken`;
|
|
986
|
+
const headers = {
|
|
987
|
+
"Content-Type": "application/json",
|
|
988
|
+
"Authorization": `Bearer ${accessToken}`
|
|
989
|
+
};
|
|
990
|
+
try {
|
|
991
|
+
const response = await fetch(endpoint, {
|
|
992
|
+
method: "POST",
|
|
993
|
+
headers,
|
|
994
|
+
body: JSON.stringify({ customToken, limitedUse })
|
|
995
|
+
});
|
|
996
|
+
if (!response.ok) {
|
|
997
|
+
const errorText = await response.text();
|
|
998
|
+
throw new Error(`App Check token exchange failed: ${response.status} ${errorText}`);
|
|
999
|
+
}
|
|
1000
|
+
const data = await response.json();
|
|
1001
|
+
return {
|
|
1002
|
+
token: data.token,
|
|
1003
|
+
ttl: data.ttl
|
|
1004
|
+
};
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
console.warn("[ternsecure - appcheck api]unexpected error:", error);
|
|
1007
|
+
throw error;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
async exchangeDebugToken(params) {
|
|
1011
|
+
const { projectId, appId, customToken, accessToken, limitedUse = false } = params;
|
|
1012
|
+
if (!projectId || !appId) {
|
|
1013
|
+
throw new Error("Project ID and App ID are required for App Check token exchange");
|
|
1014
|
+
}
|
|
1015
|
+
const endpoint = `https://firebaseappcheck.googleapis.com/v1beta/projects/${projectId}/apps/${appId}:exchangeDebugToken`;
|
|
1016
|
+
const headers = {
|
|
1017
|
+
...FIREBASE_APP_CHECK_CONFIG_HEADERS,
|
|
1018
|
+
"Authorization": `Bearer ${accessToken}`
|
|
1019
|
+
};
|
|
1020
|
+
const body = {
|
|
1021
|
+
customToken,
|
|
1022
|
+
limitedUse
|
|
1023
|
+
};
|
|
1024
|
+
try {
|
|
1025
|
+
const response = await fetch(endpoint, {
|
|
1026
|
+
method: "POST",
|
|
1027
|
+
headers,
|
|
1028
|
+
body: JSON.stringify(body)
|
|
1029
|
+
});
|
|
1030
|
+
if (!response.ok) {
|
|
1031
|
+
const errorText = await response.text();
|
|
1032
|
+
throw new Error(`App Check token exchange failed: ${response.status} ${errorText}`);
|
|
1033
|
+
}
|
|
1034
|
+
const data = await response.json();
|
|
1035
|
+
return {
|
|
1036
|
+
token: data.token,
|
|
1037
|
+
ttl: data.ttl
|
|
1038
|
+
};
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
console.warn("[ternsecure - appcheck api]unexpected error:", error);
|
|
1041
|
+
throw error;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
// src/fireRestApi/endpoints/EmailApi.ts
|
|
1047
|
+
var EmailApi = class extends AbstractAPI {
|
|
1048
|
+
async verifyEmailVerification(apiKey, params) {
|
|
1049
|
+
this.requireApiKey(apiKey);
|
|
1050
|
+
const { ...restParams } = params;
|
|
1051
|
+
return this.request({
|
|
459
1052
|
endpoint: "sendOobCode",
|
|
460
1053
|
method: "POST",
|
|
461
1054
|
bodyParams: restParams
|
|
@@ -861,673 +1454,569 @@ function mergePreDefinedOptions(userOptions = {}) {
|
|
|
861
1454
|
// src/tokens/request.ts
|
|
862
1455
|
var import_ms = require("@tern-secure/shared/ms");
|
|
863
1456
|
|
|
864
|
-
// src/jwt/
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
};
|
|
873
|
-
var RESERVED_CLAIMS = [
|
|
874
|
-
"acr",
|
|
875
|
-
"amr",
|
|
876
|
-
"at_hash",
|
|
877
|
-
"aud",
|
|
878
|
-
"auth_time",
|
|
879
|
-
"azp",
|
|
880
|
-
"cnf",
|
|
881
|
-
"c_hash",
|
|
882
|
-
"exp",
|
|
883
|
-
"firebase",
|
|
884
|
-
"iat",
|
|
885
|
-
"iss",
|
|
886
|
-
"jti",
|
|
887
|
-
"nbf",
|
|
888
|
-
"nonce",
|
|
889
|
-
"sub"
|
|
890
|
-
];
|
|
891
|
-
async function createCustomTokenJwt(uid, developerClaims) {
|
|
892
|
-
try {
|
|
893
|
-
const privateKey = process.env.FIREBASE_PRIVATE_KEY;
|
|
894
|
-
const clientEmail = process.env.FIREBASE_CLIENT_EMAIL;
|
|
895
|
-
if (!privateKey || !clientEmail) {
|
|
896
|
-
return {
|
|
897
|
-
errors: [
|
|
898
|
-
new CustomTokenError(
|
|
899
|
-
"Missing FIREBASE_PRIVATE_KEY or FIREBASE_CLIENT_EMAIL environment variables",
|
|
900
|
-
"MISSING_ENV_VARS"
|
|
901
|
-
)
|
|
902
|
-
]
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
if (!uid || typeof uid !== "string") {
|
|
906
|
-
return {
|
|
907
|
-
errors: [new CustomTokenError("uid must be a non-empty string", "INVALID_UID")]
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
if (uid.length > 128) {
|
|
911
|
-
return {
|
|
912
|
-
errors: [new CustomTokenError("uid must not exceed 128 characters", "UID_TOO_LONG")]
|
|
913
|
-
};
|
|
914
|
-
}
|
|
915
|
-
if (developerClaims) {
|
|
916
|
-
for (const claim of Object.keys(developerClaims)) {
|
|
917
|
-
if (RESERVED_CLAIMS.includes(claim)) {
|
|
918
|
-
return {
|
|
919
|
-
errors: [new CustomTokenError(`Custom claim '${claim}' is reserved`, "RESERVED_CLAIM")]
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
const expiresIn = 3600;
|
|
925
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
926
|
-
const parsedPrivateKey = await (0, import_jose.importPKCS8)(privateKey.replace(/\\n/g, "\n"), "RS256");
|
|
927
|
-
const payload = {
|
|
928
|
-
iss: clientEmail,
|
|
929
|
-
sub: clientEmail,
|
|
930
|
-
aud: "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
|
|
931
|
-
iat: now,
|
|
932
|
-
exp: now + expiresIn,
|
|
933
|
-
uid,
|
|
934
|
-
...developerClaims
|
|
935
|
-
};
|
|
936
|
-
const jwt = await new import_jose.SignJWT(payload).setProtectedHeader({ alg: "RS256", typ: "JWT" }).setIssuedAt(now).setExpirationTime(now + expiresIn).setIssuer(clientEmail).setSubject(clientEmail).setAudience(
|
|
937
|
-
"https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"
|
|
938
|
-
).sign(parsedPrivateKey);
|
|
939
|
-
return {
|
|
940
|
-
data: jwt
|
|
941
|
-
};
|
|
942
|
-
} catch (error) {
|
|
943
|
-
const message = error instanceof Error ? error.message : "Unknown error occurred";
|
|
944
|
-
return {
|
|
945
|
-
errors: [
|
|
946
|
-
new CustomTokenError(`Failed to create custom token: ${message}`, "TOKEN_CREATION_FAILED")
|
|
947
|
-
]
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
async function createCustomToken(uid, developerClaims) {
|
|
952
|
-
const { data, errors } = await createCustomTokenJwt(uid, developerClaims);
|
|
953
|
-
if (errors) {
|
|
954
|
-
throw errors[0];
|
|
955
|
-
}
|
|
956
|
-
return data;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
// src/jwt/verifyJwt.ts
|
|
960
|
-
var import_jose3 = require("jose");
|
|
961
|
-
|
|
962
|
-
// src/utils/errors.ts
|
|
963
|
-
var RefreshTokenErrorReason = {
|
|
964
|
-
NonEligibleNoCookie: "non-eligible-no-refresh-cookie",
|
|
965
|
-
NonEligibleNonGet: "non-eligible-non-get",
|
|
966
|
-
InvalidSessionToken: "invalid-session-token",
|
|
967
|
-
MissingApiClient: "missing-api-client",
|
|
968
|
-
MissingIdToken: "missing-id-token",
|
|
969
|
-
MissingSessionToken: "missing-session-token",
|
|
970
|
-
MissingRefreshToken: "missing-refresh-token",
|
|
971
|
-
ExpiredIdTokenDecodeFailed: "expired-id-token-decode-failed",
|
|
972
|
-
ExpiredSessionTokenDecodeFailed: "expired-session-token-decode-failed",
|
|
973
|
-
FetchError: "fetch-error"
|
|
974
|
-
};
|
|
975
|
-
var TokenVerificationErrorReason = {
|
|
976
|
-
TokenExpired: "token-expired",
|
|
977
|
-
TokenInvalid: "token-invalid",
|
|
978
|
-
TokenInvalidAlgorithm: "token-invalid-algorithm",
|
|
979
|
-
TokenInvalidAuthorizedParties: "token-invalid-authorized-parties",
|
|
980
|
-
TokenInvalidSignature: "token-invalid-signature",
|
|
981
|
-
TokenNotActiveYet: "token-not-active-yet",
|
|
982
|
-
TokenIatInTheFuture: "token-iat-in-the-future",
|
|
983
|
-
TokenVerificationFailed: "token-verification-failed",
|
|
984
|
-
InvalidSecretKey: "secret-key-invalid",
|
|
985
|
-
LocalJWKMissing: "jwk-local-missing",
|
|
986
|
-
RemoteJWKFailedToLoad: "jwk-remote-failed-to-load",
|
|
987
|
-
RemoteJWKInvalid: "jwk-remote-invalid",
|
|
988
|
-
RemoteJWKMissing: "jwk-remote-missing",
|
|
989
|
-
JWKFailedToResolve: "jwk-failed-to-resolve",
|
|
990
|
-
JWKKidMismatch: "jwk-kid-mismatch"
|
|
991
|
-
};
|
|
992
|
-
var TokenVerificationError = class _TokenVerificationError extends Error {
|
|
993
|
-
reason;
|
|
994
|
-
tokenCarrier;
|
|
995
|
-
constructor({
|
|
996
|
-
message,
|
|
997
|
-
reason
|
|
998
|
-
}) {
|
|
999
|
-
super(message);
|
|
1000
|
-
Object.setPrototypeOf(this, _TokenVerificationError.prototype);
|
|
1001
|
-
this.reason = reason;
|
|
1002
|
-
this.message = message;
|
|
1003
|
-
}
|
|
1004
|
-
getFullMessage() {
|
|
1005
|
-
return `${[this.message].filter((m) => m).join(" ")} (reason=${this.reason}, token-carrier=${this.tokenCarrier})`;
|
|
1006
|
-
}
|
|
1007
|
-
};
|
|
1008
|
-
|
|
1009
|
-
// src/utils/rfc4648.ts
|
|
1010
|
-
var base64url = {
|
|
1011
|
-
parse(string, opts) {
|
|
1012
|
-
return parse2(string, base64UrlEncoding, opts);
|
|
1013
|
-
},
|
|
1014
|
-
stringify(data, opts) {
|
|
1015
|
-
return stringify(data, base64UrlEncoding, opts);
|
|
1016
|
-
}
|
|
1017
|
-
};
|
|
1018
|
-
var base64UrlEncoding = {
|
|
1019
|
-
chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
|
|
1020
|
-
bits: 6
|
|
1021
|
-
};
|
|
1022
|
-
function parse2(string, encoding, opts = {}) {
|
|
1023
|
-
if (!encoding.codes) {
|
|
1024
|
-
encoding.codes = {};
|
|
1025
|
-
for (let i = 0; i < encoding.chars.length; ++i) {
|
|
1026
|
-
encoding.codes[encoding.chars[i]] = i;
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
if (!opts.loose && string.length * encoding.bits & 7) {
|
|
1030
|
-
throw new SyntaxError("Invalid padding");
|
|
1031
|
-
}
|
|
1032
|
-
let end = string.length;
|
|
1033
|
-
while (string[end - 1] === "=") {
|
|
1034
|
-
--end;
|
|
1035
|
-
if (!opts.loose && !((string.length - end) * encoding.bits & 7)) {
|
|
1036
|
-
throw new SyntaxError("Invalid padding");
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
const out = new (opts.out ?? Uint8Array)(end * encoding.bits / 8 | 0);
|
|
1040
|
-
let bits = 0;
|
|
1041
|
-
let buffer = 0;
|
|
1042
|
-
let written = 0;
|
|
1043
|
-
for (let i = 0; i < end; ++i) {
|
|
1044
|
-
const value = encoding.codes[string[i]];
|
|
1045
|
-
if (value === void 0) {
|
|
1046
|
-
throw new SyntaxError("Invalid character " + string[i]);
|
|
1047
|
-
}
|
|
1048
|
-
buffer = buffer << encoding.bits | value;
|
|
1049
|
-
bits += encoding.bits;
|
|
1050
|
-
if (bits >= 8) {
|
|
1051
|
-
bits -= 8;
|
|
1052
|
-
out[written++] = 255 & buffer >> bits;
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
if (bits >= encoding.bits || 255 & buffer << 8 - bits) {
|
|
1056
|
-
throw new SyntaxError("Unexpected end of data");
|
|
1057
|
-
}
|
|
1058
|
-
return out;
|
|
1059
|
-
}
|
|
1060
|
-
function stringify(data, encoding, opts = {}) {
|
|
1061
|
-
const { pad = true } = opts;
|
|
1062
|
-
const mask = (1 << encoding.bits) - 1;
|
|
1063
|
-
let out = "";
|
|
1064
|
-
let bits = 0;
|
|
1065
|
-
let buffer = 0;
|
|
1066
|
-
for (let i = 0; i < data.length; ++i) {
|
|
1067
|
-
buffer = buffer << 8 | 255 & data[i];
|
|
1068
|
-
bits += 8;
|
|
1069
|
-
while (bits > encoding.bits) {
|
|
1070
|
-
bits -= encoding.bits;
|
|
1071
|
-
out += encoding.chars[mask & buffer >> bits];
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
if (bits) {
|
|
1075
|
-
out += encoding.chars[mask & buffer << encoding.bits - bits];
|
|
1076
|
-
}
|
|
1077
|
-
if (pad) {
|
|
1078
|
-
while (out.length * encoding.bits & 7) {
|
|
1079
|
-
out += "=";
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
return out;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
// src/jwt/cryptoKeys.ts
|
|
1086
|
-
var import_jose2 = require("jose");
|
|
1087
|
-
async function importKey(key, algorithm) {
|
|
1088
|
-
if (typeof key === "object") {
|
|
1089
|
-
const result = await (0, import_jose2.importJWK)(key, algorithm);
|
|
1090
|
-
if (result instanceof Uint8Array) {
|
|
1091
|
-
throw new Error("Unexpected Uint8Array result from JWK import");
|
|
1092
|
-
}
|
|
1093
|
-
return result;
|
|
1094
|
-
}
|
|
1095
|
-
const keyString = key.trim();
|
|
1096
|
-
if (keyString.includes("-----BEGIN CERTIFICATE-----")) {
|
|
1097
|
-
return await (0, import_jose2.importX509)(keyString, algorithm);
|
|
1098
|
-
}
|
|
1099
|
-
if (keyString.includes("-----BEGIN PUBLIC KEY-----")) {
|
|
1100
|
-
return await (0, import_jose2.importSPKI)(keyString, algorithm);
|
|
1101
|
-
}
|
|
1102
|
-
try {
|
|
1103
|
-
return await (0, import_jose2.importSPKI)(keyString, algorithm);
|
|
1104
|
-
} catch (error) {
|
|
1105
|
-
throw new Error(
|
|
1106
|
-
`Unsupported key format. Supported formats: X.509 certificate (PEM), SPKI (PEM), JWK (JSON object or string). Error: ${error}`
|
|
1107
|
-
);
|
|
1108
|
-
}
|
|
1457
|
+
// src/jwt/guardReturn.ts
|
|
1458
|
+
function createJwtGuard(decodedFn) {
|
|
1459
|
+
return (...args) => {
|
|
1460
|
+
const { data, errors } = decodedFn(...args);
|
|
1461
|
+
if (errors) {
|
|
1462
|
+
throw errors[0];
|
|
1463
|
+
}
|
|
1464
|
+
return data;
|
|
1465
|
+
};
|
|
1109
1466
|
}
|
|
1110
1467
|
|
|
1111
|
-
// src/jwt/
|
|
1112
|
-
var
|
|
1113
|
-
RS256: "SHA-256",
|
|
1114
|
-
RS384: "SHA-384",
|
|
1115
|
-
RS512: "SHA-512"
|
|
1116
|
-
};
|
|
1117
|
-
var algs = Object.keys(algToHash);
|
|
1468
|
+
// src/jwt/jwt.ts
|
|
1469
|
+
var import_jose3 = require("jose");
|
|
1118
1470
|
|
|
1119
|
-
// src/jwt/
|
|
1120
|
-
var
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
reason: TokenVerificationErrorReason.TokenInvalid,
|
|
1127
|
-
message: `Invalid JWT kid ${JSON.stringify(kid)}. Expected a string.`
|
|
1128
|
-
});
|
|
1129
|
-
}
|
|
1130
|
-
};
|
|
1131
|
-
var verifySubClaim = (sub) => {
|
|
1132
|
-
if (typeof sub !== "string") {
|
|
1133
|
-
throw new TokenVerificationError({
|
|
1134
|
-
reason: TokenVerificationErrorReason.TokenVerificationFailed,
|
|
1135
|
-
message: `Subject claim (sub) is required and must be a string. Received ${JSON.stringify(sub)}.`
|
|
1136
|
-
});
|
|
1137
|
-
}
|
|
1138
|
-
};
|
|
1139
|
-
var verifyExpirationClaim = (exp, clockSkewInMs) => {
|
|
1140
|
-
if (typeof exp !== "number") {
|
|
1141
|
-
throw new TokenVerificationError({
|
|
1142
|
-
reason: TokenVerificationErrorReason.TokenVerificationFailed,
|
|
1143
|
-
message: `Invalid JWT expiry date (exp) claim ${JSON.stringify(exp)}. Expected a number.`
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
1146
|
-
const currentDate = new Date(Date.now());
|
|
1147
|
-
const expiryDate = /* @__PURE__ */ new Date(0);
|
|
1148
|
-
expiryDate.setUTCSeconds(exp);
|
|
1149
|
-
const expired = expiryDate.getTime() <= currentDate.getTime() - clockSkewInMs;
|
|
1150
|
-
if (expired) {
|
|
1151
|
-
throw new TokenVerificationError({
|
|
1152
|
-
reason: TokenVerificationErrorReason.TokenExpired,
|
|
1153
|
-
message: `JWT is expired. Expiry date: ${expiryDate.toUTCString()}, Current date: ${currentDate.toUTCString()}.`
|
|
1154
|
-
});
|
|
1155
|
-
}
|
|
1156
|
-
};
|
|
1157
|
-
var verifyIssuedAtClaim = (iat, clockSkewInMs) => {
|
|
1158
|
-
if (typeof iat === "undefined") {
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
if (typeof iat !== "number") {
|
|
1162
|
-
throw new TokenVerificationError({
|
|
1163
|
-
reason: TokenVerificationErrorReason.TokenVerificationFailed,
|
|
1164
|
-
message: `Invalid JWT issued at date claim (iat) ${JSON.stringify(iat)}. Expected a number.`
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
const currentDate = new Date(Date.now());
|
|
1168
|
-
const issuedAtDate = /* @__PURE__ */ new Date(0);
|
|
1169
|
-
issuedAtDate.setUTCSeconds(iat);
|
|
1170
|
-
const postIssued = issuedAtDate.getTime() > currentDate.getTime() + clockSkewInMs;
|
|
1171
|
-
if (postIssued) {
|
|
1172
|
-
throw new TokenVerificationError({
|
|
1173
|
-
reason: TokenVerificationErrorReason.TokenIatInTheFuture,
|
|
1174
|
-
message: `JWT issued at date claim (iat) is in the future. Issued at date: ${issuedAtDate.toUTCString()}; Current date: ${currentDate.toUTCString()};`
|
|
1175
|
-
});
|
|
1471
|
+
// src/jwt/customJwt.ts
|
|
1472
|
+
var import_jose4 = require("jose");
|
|
1473
|
+
var CustomTokenError = class extends Error {
|
|
1474
|
+
constructor(message, code) {
|
|
1475
|
+
super(message);
|
|
1476
|
+
this.code = code;
|
|
1477
|
+
this.name = "CustomTokenError";
|
|
1176
1478
|
}
|
|
1177
1479
|
};
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
function ternDecodeJwt(token) {
|
|
1480
|
+
var RESERVED_CLAIMS = [
|
|
1481
|
+
"acr",
|
|
1482
|
+
"amr",
|
|
1483
|
+
"at_hash",
|
|
1484
|
+
"aud",
|
|
1485
|
+
"auth_time",
|
|
1486
|
+
"azp",
|
|
1487
|
+
"cnf",
|
|
1488
|
+
"c_hash",
|
|
1489
|
+
"exp",
|
|
1490
|
+
"firebase",
|
|
1491
|
+
"iat",
|
|
1492
|
+
"iss",
|
|
1493
|
+
"jti",
|
|
1494
|
+
"nbf",
|
|
1495
|
+
"nonce",
|
|
1496
|
+
"sub"
|
|
1497
|
+
];
|
|
1498
|
+
async function createCustomTokenJwt(uid, developerClaims) {
|
|
1200
1499
|
try {
|
|
1201
|
-
const
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1204
|
-
if (tokenParts.length !== 3) {
|
|
1500
|
+
const privateKey = process.env.FIREBASE_PRIVATE_KEY;
|
|
1501
|
+
const clientEmail = process.env.FIREBASE_CLIENT_EMAIL;
|
|
1502
|
+
if (!privateKey || !clientEmail) {
|
|
1205
1503
|
return {
|
|
1206
1504
|
errors: [
|
|
1207
|
-
new
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1505
|
+
new CustomTokenError(
|
|
1506
|
+
"Missing FIREBASE_PRIVATE_KEY or FIREBASE_CLIENT_EMAIL environment variables",
|
|
1507
|
+
"MISSING_ENV_VARS"
|
|
1508
|
+
)
|
|
1211
1509
|
]
|
|
1212
1510
|
};
|
|
1213
1511
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1512
|
+
if (!uid || typeof uid !== "string") {
|
|
1513
|
+
return {
|
|
1514
|
+
errors: [new CustomTokenError("uid must be a non-empty string", "INVALID_UID")]
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
if (uid.length > 128) {
|
|
1518
|
+
return {
|
|
1519
|
+
errors: [new CustomTokenError("uid must not exceed 128 characters", "UID_TOO_LONG")]
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
if (developerClaims) {
|
|
1523
|
+
for (const claim of Object.keys(developerClaims)) {
|
|
1524
|
+
if (RESERVED_CLAIMS.includes(claim)) {
|
|
1525
|
+
return {
|
|
1526
|
+
errors: [new CustomTokenError(`Custom claim '${claim}' is reserved`, "RESERVED_CLAIM")]
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1225
1529
|
}
|
|
1530
|
+
}
|
|
1531
|
+
const expiresIn = 3600;
|
|
1532
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1533
|
+
const parsedPrivateKey = await (0, import_jose4.importPKCS8)(privateKey.replace(/\\n/g, "\n"), "RS256");
|
|
1534
|
+
const payload = {
|
|
1535
|
+
iss: clientEmail,
|
|
1536
|
+
sub: clientEmail,
|
|
1537
|
+
aud: "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
|
|
1538
|
+
iat: now,
|
|
1539
|
+
exp: now + expiresIn,
|
|
1540
|
+
uid,
|
|
1541
|
+
...developerClaims
|
|
1542
|
+
};
|
|
1543
|
+
const jwt = await new import_jose4.SignJWT(payload).setProtectedHeader({ alg: "RS256", typ: "JWT" }).setIssuedAt(now).setExpirationTime(now + expiresIn).setIssuer(clientEmail).setSubject(clientEmail).setAudience(
|
|
1544
|
+
"https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"
|
|
1545
|
+
).sign(parsedPrivateKey);
|
|
1546
|
+
return {
|
|
1547
|
+
data: jwt
|
|
1226
1548
|
};
|
|
1227
|
-
return { data };
|
|
1228
1549
|
} catch (error) {
|
|
1550
|
+
const message = error instanceof Error ? error.message : "Unknown error occurred";
|
|
1229
1551
|
return {
|
|
1230
1552
|
errors: [
|
|
1231
|
-
new
|
|
1232
|
-
reason: TokenVerificationErrorReason.TokenInvalid,
|
|
1233
|
-
message: `${error.message || "Invalid Token or Protected Header formatting"} (Token length: ${token?.length}, First 10 chars: ${token?.substring(0, 10)}...)`
|
|
1234
|
-
})
|
|
1553
|
+
new CustomTokenError(`Failed to create custom token: ${message}`, "TOKEN_CREATION_FAILED")
|
|
1235
1554
|
]
|
|
1236
1555
|
};
|
|
1237
1556
|
}
|
|
1238
1557
|
}
|
|
1239
|
-
async function
|
|
1240
|
-
const {
|
|
1241
|
-
const clockSkew = options.clockSkewInMs || DEFAULT_CLOCK_SKEW_IN_MS;
|
|
1242
|
-
const { data: decoded, errors } = ternDecodeJwt(token);
|
|
1558
|
+
async function createCustomToken(uid, developerClaims) {
|
|
1559
|
+
const { data, errors } = await createCustomTokenJwt(uid, developerClaims);
|
|
1243
1560
|
if (errors) {
|
|
1244
|
-
|
|
1245
|
-
}
|
|
1246
|
-
const { header, payload } = decoded;
|
|
1247
|
-
try {
|
|
1248
|
-
verifyHeaderKid(header.kid);
|
|
1249
|
-
verifySubClaim(payload.sub);
|
|
1250
|
-
verifyExpirationClaim(payload.exp, clockSkew);
|
|
1251
|
-
verifyIssuedAtClaim(payload.iat, clockSkew);
|
|
1252
|
-
} catch (error) {
|
|
1253
|
-
return { errors: [error] };
|
|
1254
|
-
}
|
|
1255
|
-
const { data: verifiedPayload, errors: signatureErrors } = await verifySignature(decoded, key);
|
|
1256
|
-
if (signatureErrors) {
|
|
1257
|
-
return {
|
|
1258
|
-
errors: [
|
|
1259
|
-
new TokenVerificationError({
|
|
1260
|
-
reason: TokenVerificationErrorReason.TokenInvalidSignature,
|
|
1261
|
-
message: "Token signature verification failed."
|
|
1262
|
-
})
|
|
1263
|
-
]
|
|
1264
|
-
};
|
|
1561
|
+
throw errors[0];
|
|
1265
1562
|
}
|
|
1266
|
-
|
|
1267
|
-
return { data: decodedIdToken };
|
|
1563
|
+
return data;
|
|
1268
1564
|
}
|
|
1269
1565
|
|
|
1270
|
-
// src/
|
|
1271
|
-
var
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
function
|
|
1275
|
-
|
|
1566
|
+
// src/jwt/signJwt.ts
|
|
1567
|
+
var import_jose5 = require("jose");
|
|
1568
|
+
|
|
1569
|
+
// src/utils/fetcher.ts
|
|
1570
|
+
async function getDetailFromResponse(response) {
|
|
1571
|
+
const json = await response.json();
|
|
1572
|
+
if (!json) {
|
|
1573
|
+
return "Missing error payload";
|
|
1574
|
+
}
|
|
1575
|
+
let detail = typeof json.error === "string" ? json.error : json.error?.message ?? "Missing error payload";
|
|
1576
|
+
if (json.error_description) {
|
|
1577
|
+
detail += " (" + json.error_description + ")";
|
|
1578
|
+
}
|
|
1579
|
+
return detail;
|
|
1276
1580
|
}
|
|
1277
|
-
function
|
|
1278
|
-
return
|
|
1581
|
+
async function fetchText(url, init) {
|
|
1582
|
+
return (await fetchAny(url, init)).text();
|
|
1279
1583
|
}
|
|
1280
|
-
function
|
|
1281
|
-
|
|
1282
|
-
lastUpdatedAt = shouldExpire ? Date.now() : -1;
|
|
1584
|
+
async function fetchJson(url, init) {
|
|
1585
|
+
return (await fetchAny(url, init)).json();
|
|
1283
1586
|
}
|
|
1284
|
-
async function
|
|
1285
|
-
const
|
|
1286
|
-
const response = await fetch(url);
|
|
1587
|
+
async function fetchAny(url, init) {
|
|
1588
|
+
const response = await fetch(url, init);
|
|
1287
1589
|
if (!response.ok) {
|
|
1590
|
+
throw new Error(await getDetailFromResponse(response));
|
|
1591
|
+
}
|
|
1592
|
+
return response;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// src/jwt/types.ts
|
|
1596
|
+
var ALGORITHM_RS256 = "RS256";
|
|
1597
|
+
|
|
1598
|
+
// src/jwt/signJwt.ts
|
|
1599
|
+
async function ternSignJwt(opts) {
|
|
1600
|
+
const { payload, privateKey, keyId } = opts;
|
|
1601
|
+
let key;
|
|
1602
|
+
try {
|
|
1603
|
+
key = await (0, import_jose5.importPKCS8)(privateKey, ALGORITHM_RS256);
|
|
1604
|
+
} catch (error) {
|
|
1288
1605
|
throw new TokenVerificationError({
|
|
1289
|
-
message: `
|
|
1606
|
+
message: `Failed to import private key: ${error.message}`,
|
|
1290
1607
|
reason: TokenVerificationErrorReason.TokenInvalid
|
|
1291
1608
|
});
|
|
1292
1609
|
}
|
|
1293
|
-
|
|
1294
|
-
const expiresAt = getExpiresAt(response);
|
|
1295
|
-
return {
|
|
1296
|
-
keys: data,
|
|
1297
|
-
expiresAt
|
|
1298
|
-
};
|
|
1610
|
+
return new import_jose5.SignJWT(payload).setProtectedHeader({ alg: ALGORITHM_RS256, kid: keyId }).sign(key);
|
|
1299
1611
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1612
|
+
function formatBase64(value) {
|
|
1613
|
+
return value.replace(/\//g, "_").replace(/\+/g, "-").replace(/=+$/, "");
|
|
1614
|
+
}
|
|
1615
|
+
function encodeSegment(segment) {
|
|
1616
|
+
const value = JSON.stringify(segment);
|
|
1617
|
+
return formatBase64(import_jose5.base64url.encode(value));
|
|
1618
|
+
}
|
|
1619
|
+
async function ternSignBlob({
|
|
1620
|
+
payload,
|
|
1621
|
+
serviceAccountId,
|
|
1622
|
+
accessToken
|
|
1304
1623
|
}) {
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1624
|
+
const url = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccountId}:signBlob`;
|
|
1625
|
+
const header = {
|
|
1626
|
+
alg: ALGORITHM_RS256,
|
|
1627
|
+
typ: "JWT"
|
|
1628
|
+
};
|
|
1629
|
+
const token = `${encodeSegment(header)}.${encodeSegment(payload)}`;
|
|
1630
|
+
const request = {
|
|
1631
|
+
method: "POST",
|
|
1632
|
+
headers: {
|
|
1633
|
+
Authorization: `Bearer ${accessToken}`
|
|
1634
|
+
},
|
|
1635
|
+
body: JSON.stringify({ payload: import_jose5.base64url.encode(token) })
|
|
1636
|
+
};
|
|
1637
|
+
const response = await fetchAny(url, request);
|
|
1638
|
+
const blob = await response.blob();
|
|
1639
|
+
const key = await blob.text();
|
|
1640
|
+
const { signedBlob } = JSON.parse(key);
|
|
1641
|
+
return `${token}.${formatBase64(signedBlob)}`;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// src/jwt/crypto-signer.ts
|
|
1645
|
+
var ServiceAccountSigner = class {
|
|
1646
|
+
constructor(credential, tenantId) {
|
|
1647
|
+
this.credential = credential;
|
|
1648
|
+
this.tenantId = tenantId;
|
|
1649
|
+
}
|
|
1650
|
+
async getAccountId() {
|
|
1651
|
+
return Promise.resolve(this.credential.clientEmail);
|
|
1652
|
+
}
|
|
1653
|
+
async sign(payload) {
|
|
1654
|
+
if (this.tenantId) {
|
|
1655
|
+
payload.tenant_id = this.tenantId;
|
|
1312
1656
|
}
|
|
1313
|
-
|
|
1314
|
-
Object.entries(keys).forEach(([keyId, cert2]) => {
|
|
1315
|
-
setInCache(keyId, cert2);
|
|
1316
|
-
});
|
|
1657
|
+
return ternSignJwt({ payload, privateKey: this.credential.privateKey });
|
|
1317
1658
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1659
|
+
};
|
|
1660
|
+
var IAMSigner = class {
|
|
1661
|
+
algorithm = ALGORITHM_RS256;
|
|
1662
|
+
credential;
|
|
1663
|
+
tenantId;
|
|
1664
|
+
serviceAccountId;
|
|
1665
|
+
constructor(credential, tenantId, serviceAccountId) {
|
|
1666
|
+
this.credential = credential;
|
|
1667
|
+
this.tenantId = tenantId;
|
|
1668
|
+
this.serviceAccountId = serviceAccountId;
|
|
1669
|
+
}
|
|
1670
|
+
async sign(payload) {
|
|
1671
|
+
if (this.tenantId) {
|
|
1672
|
+
payload.tenant_id = this.tenantId;
|
|
1673
|
+
}
|
|
1674
|
+
const serviceAccount = await this.getAccountId();
|
|
1675
|
+
const accessToken = await this.credential.getAccessToken();
|
|
1676
|
+
return ternSignBlob({
|
|
1677
|
+
accessToken: accessToken.accessToken,
|
|
1678
|
+
serviceAccountId: serviceAccount,
|
|
1679
|
+
payload
|
|
1325
1680
|
});
|
|
1326
1681
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1682
|
+
async getAccountId() {
|
|
1683
|
+
if (this.serviceAccountId) {
|
|
1684
|
+
return this.serviceAccountId;
|
|
1685
|
+
}
|
|
1686
|
+
const token = await this.credential.getAccessToken();
|
|
1687
|
+
const url = "http://metadata/computeMetadata/v1/instance/service-accounts/default/email";
|
|
1688
|
+
const request = {
|
|
1689
|
+
method: "GET",
|
|
1690
|
+
headers: {
|
|
1691
|
+
"Metadata-Flavor": "Google",
|
|
1692
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
return this.serviceAccountId = await fetchText(url, request);
|
|
1341
1696
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1697
|
+
};
|
|
1698
|
+
|
|
1699
|
+
// src/jwt/index.ts
|
|
1700
|
+
var ternDecodeJwt2 = createJwtGuard(ternDecodeJwt);
|
|
1701
|
+
|
|
1702
|
+
// src/utils/token-generator.ts
|
|
1703
|
+
function cryptoSignerFromCredential(credential, tenantId, serviceAccountId) {
|
|
1704
|
+
if (credential instanceof ServiceAccountManager) {
|
|
1705
|
+
return new ServiceAccountSigner(credential, tenantId);
|
|
1348
1706
|
}
|
|
1349
|
-
|
|
1350
|
-
const maxAge = maxAgeMatch ? parseInt(maxAgeMatch[1], 10) : DEFAULT_CACHE_DURATION / 1e3;
|
|
1351
|
-
return Date.now() + maxAge * 1e3;
|
|
1707
|
+
return new IAMSigner(credential, tenantId, serviceAccountId);
|
|
1352
1708
|
}
|
|
1353
1709
|
|
|
1354
|
-
// src/
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1710
|
+
// src/app-check/AppCheckApi.ts
|
|
1711
|
+
function getSdkVersion2() {
|
|
1712
|
+
return "12.7.0";
|
|
1713
|
+
}
|
|
1714
|
+
var FIREBASE_APP_CHECK_CONFIG_HEADERS2 = {
|
|
1715
|
+
"X-Firebase-Client": `fire-admin-node/${getSdkVersion2()}`
|
|
1716
|
+
};
|
|
1717
|
+
var AppCheckApi2 = class {
|
|
1718
|
+
constructor(credential) {
|
|
1719
|
+
this.credential = credential;
|
|
1359
1720
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1721
|
+
async exchangeToken(params) {
|
|
1722
|
+
const { projectId, appId, customToken, limitedUse = false } = params;
|
|
1723
|
+
const token = await this.credential.getAccessToken(false);
|
|
1724
|
+
if (!projectId || !appId) {
|
|
1725
|
+
throw new Error("Project ID and App ID are required for App Check token exchange");
|
|
1726
|
+
}
|
|
1727
|
+
const endpoint = `https://firebaseappcheck.googleapis.com/v1/projects/${projectId}/apps/${appId}:exchangeCustomToken`;
|
|
1728
|
+
const headers = {
|
|
1729
|
+
"Content-Type": "application/json",
|
|
1730
|
+
"Authorization": `Bearer ${token.accessToken}`
|
|
1370
1731
|
};
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1732
|
+
try {
|
|
1733
|
+
const response = await fetch(endpoint, {
|
|
1734
|
+
method: "POST",
|
|
1735
|
+
headers,
|
|
1736
|
+
body: JSON.stringify({ customToken, limitedUse })
|
|
1737
|
+
});
|
|
1738
|
+
if (!response.ok) {
|
|
1739
|
+
const errorText = await response.text();
|
|
1740
|
+
throw new Error(`App Check token exchange failed: ${response.status} ${errorText}`);
|
|
1741
|
+
}
|
|
1742
|
+
const data = await response.json();
|
|
1375
1743
|
return {
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
reason: TokenVerificationErrorReason.TokenInvalid,
|
|
1379
|
-
message: `No public key found for kid "${kid}".`
|
|
1380
|
-
})
|
|
1381
|
-
]
|
|
1744
|
+
token: data.token,
|
|
1745
|
+
ttl: data.ttl
|
|
1382
1746
|
};
|
|
1747
|
+
} catch (error) {
|
|
1748
|
+
console.warn("[ternsecure - appcheck api]unexpected error:", error);
|
|
1749
|
+
throw error;
|
|
1383
1750
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1751
|
+
}
|
|
1752
|
+
async exchangeDebugToken(params) {
|
|
1753
|
+
const { projectId, appId, customToken, accessToken, limitedUse = false } = params;
|
|
1754
|
+
if (!projectId || !appId) {
|
|
1755
|
+
throw new Error("Project ID and App ID are required for App Check token exchange");
|
|
1388
1756
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1757
|
+
const endpoint = `https://firebaseappcheck.googleapis.com/v1beta/projects/${projectId}/apps/${appId}:exchangeDebugToken`;
|
|
1758
|
+
const headers = {
|
|
1759
|
+
...FIREBASE_APP_CHECK_CONFIG_HEADERS2,
|
|
1760
|
+
"Authorization": `Bearer ${accessToken}`
|
|
1761
|
+
};
|
|
1762
|
+
const body = {
|
|
1763
|
+
customToken,
|
|
1764
|
+
limitedUse
|
|
1391
1765
|
};
|
|
1766
|
+
try {
|
|
1767
|
+
const response = await fetch(endpoint, {
|
|
1768
|
+
method: "POST",
|
|
1769
|
+
headers,
|
|
1770
|
+
body: JSON.stringify(body)
|
|
1771
|
+
});
|
|
1772
|
+
if (!response.ok) {
|
|
1773
|
+
const errorText = await response.text();
|
|
1774
|
+
throw new Error(`App Check token exchange failed: ${response.status} ${errorText}`);
|
|
1775
|
+
}
|
|
1776
|
+
const data = await response.json();
|
|
1777
|
+
return {
|
|
1778
|
+
token: data.token,
|
|
1779
|
+
ttl: data.ttl
|
|
1780
|
+
};
|
|
1781
|
+
} catch (error) {
|
|
1782
|
+
console.warn("[ternsecure - appcheck api]unexpected error:", error);
|
|
1783
|
+
throw error;
|
|
1784
|
+
}
|
|
1392
1785
|
}
|
|
1786
|
+
};
|
|
1787
|
+
|
|
1788
|
+
// src/app-check/generator.ts
|
|
1789
|
+
function transformMillisecondsToSecondsString(milliseconds) {
|
|
1790
|
+
let duration;
|
|
1791
|
+
const seconds = Math.floor(milliseconds / 1e3);
|
|
1792
|
+
const nanos = Math.floor((milliseconds - seconds * 1e3) * 1e6);
|
|
1793
|
+
if (nanos > 0) {
|
|
1794
|
+
let nanoString = nanos.toString();
|
|
1795
|
+
while (nanoString.length < 9) {
|
|
1796
|
+
nanoString = "0" + nanoString;
|
|
1797
|
+
}
|
|
1798
|
+
duration = `${seconds}.${nanoString}s`;
|
|
1799
|
+
} else {
|
|
1800
|
+
duration = `${seconds}s`;
|
|
1801
|
+
}
|
|
1802
|
+
return duration;
|
|
1393
1803
|
}
|
|
1804
|
+
var AppCheckTokenGenerator = class {
|
|
1805
|
+
signer;
|
|
1806
|
+
constructor(signer) {
|
|
1807
|
+
this.signer = signer;
|
|
1808
|
+
}
|
|
1809
|
+
async createCustomToken(appId, options) {
|
|
1810
|
+
if (!appId) {
|
|
1811
|
+
throw new Error(
|
|
1812
|
+
"appId is invalid"
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
let customOptions = {};
|
|
1816
|
+
if (typeof options !== "undefined") {
|
|
1817
|
+
customOptions = this.validateTokenOptions(options);
|
|
1818
|
+
}
|
|
1819
|
+
const account = await this.signer.getAccountId();
|
|
1820
|
+
const iat = Math.floor(Date.now() / 1e3);
|
|
1821
|
+
const body = {
|
|
1822
|
+
iss: account,
|
|
1823
|
+
sub: account,
|
|
1824
|
+
app_id: appId,
|
|
1825
|
+
aud: FIREBASE_APP_CHECK_AUDIENCE,
|
|
1826
|
+
exp: iat + ONE_MINUTE_IN_SECONDS * 5,
|
|
1827
|
+
iat,
|
|
1828
|
+
...customOptions
|
|
1829
|
+
};
|
|
1830
|
+
return this.signer.sign(body);
|
|
1831
|
+
}
|
|
1832
|
+
validateTokenOptions(options) {
|
|
1833
|
+
if (typeof options.ttlMillis !== "undefined") {
|
|
1834
|
+
if (options.ttlMillis < ONE_MINUTE_IN_MILLIS * 30 || options.ttlMillis > ONE_DAY_IN_MILLIS * 7) {
|
|
1835
|
+
throw new Error(
|
|
1836
|
+
"ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive)."
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
return { ttl: transformMillisecondsToSecondsString(options.ttlMillis) };
|
|
1840
|
+
}
|
|
1841
|
+
return {};
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1394
1844
|
|
|
1395
|
-
// src/
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1845
|
+
// src/app-check/serverAppCheck.ts
|
|
1846
|
+
var import_redis = require("@upstash/redis");
|
|
1847
|
+
|
|
1848
|
+
// src/admin/sessionTernSecure.ts
|
|
1849
|
+
var import_errors6 = require("@tern-secure/shared/errors");
|
|
1850
|
+
|
|
1851
|
+
// src/utils/admin-init.ts
|
|
1852
|
+
var import_firebase_admin = __toESM(require("firebase-admin"));
|
|
1853
|
+
var import_app_check = require("firebase-admin/app-check");
|
|
1854
|
+
|
|
1855
|
+
// src/utils/config.ts
|
|
1856
|
+
var loadAdminConfig = () => ({
|
|
1857
|
+
projectId: process.env.FIREBASE_PROJECT_ID || "",
|
|
1858
|
+
clientEmail: process.env.FIREBASE_CLIENT_EMAIL || "",
|
|
1859
|
+
privateKey: process.env.FIREBASE_PRIVATE_KEY || ""
|
|
1860
|
+
});
|
|
1861
|
+
var validateAdminConfig = (config) => {
|
|
1862
|
+
const requiredFields = [
|
|
1863
|
+
"projectId",
|
|
1864
|
+
"clientEmail",
|
|
1865
|
+
"privateKey"
|
|
1866
|
+
];
|
|
1867
|
+
const errors = [];
|
|
1868
|
+
requiredFields.forEach((field) => {
|
|
1869
|
+
if (!config[field]) {
|
|
1870
|
+
errors.push(`Missing required field: FIREBASE_${String(field).toUpperCase()}`);
|
|
1401
1871
|
}
|
|
1402
|
-
|
|
1872
|
+
});
|
|
1873
|
+
return {
|
|
1874
|
+
isValid: errors.length === 0,
|
|
1875
|
+
errors,
|
|
1876
|
+
config
|
|
1403
1877
|
};
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1878
|
+
};
|
|
1879
|
+
var initializeAdminConfig = () => {
|
|
1880
|
+
const config = loadAdminConfig();
|
|
1881
|
+
const validationResult = validateAdminConfig(config);
|
|
1882
|
+
if (!validationResult.isValid) {
|
|
1883
|
+
throw new Error(
|
|
1884
|
+
`Firebase Admin configuration validation failed:
|
|
1885
|
+
${validationResult.errors.join("\n")}`
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
return config;
|
|
1889
|
+
};
|
|
1408
1890
|
|
|
1409
|
-
// src/
|
|
1410
|
-
|
|
1411
|
-
var ALGORITHM_RS256 = "RS256";
|
|
1412
|
-
async function ternSignJwt(opts) {
|
|
1413
|
-
const { payload, privateKey, keyId } = opts;
|
|
1414
|
-
let key;
|
|
1891
|
+
// src/utils/admin-init.ts
|
|
1892
|
+
if (!import_firebase_admin.default.apps.length) {
|
|
1415
1893
|
try {
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1894
|
+
const config = initializeAdminConfig();
|
|
1895
|
+
import_firebase_admin.default.initializeApp({
|
|
1896
|
+
credential: import_firebase_admin.default.credential.cert({
|
|
1897
|
+
...config,
|
|
1898
|
+
privateKey: config.privateKey.replace(/\\n/g, "\n")
|
|
1899
|
+
})
|
|
1421
1900
|
});
|
|
1901
|
+
} catch (error) {
|
|
1902
|
+
console.error("Firebase admin initialization error", error);
|
|
1422
1903
|
}
|
|
1423
|
-
return new import_jose5.SignJWT(payload).setProtectedHeader({ alg: ALGORITHM_RS256, kid: keyId }).sign(key);
|
|
1424
1904
|
}
|
|
1905
|
+
var adminTernSecureAuth = import_firebase_admin.default.auth();
|
|
1906
|
+
var adminTernSecureDb = import_firebase_admin.default.firestore();
|
|
1907
|
+
var TernSecureTenantManager = import_firebase_admin.default.auth().tenantManager();
|
|
1908
|
+
var appCheckAdmin = (0, import_app_check.getAppCheck)();
|
|
1425
1909
|
|
|
1426
|
-
// src/
|
|
1427
|
-
var
|
|
1910
|
+
// src/admin/sessionTernSecure.ts
|
|
1911
|
+
var DEFAULT_COOKIE_CONFIG = {
|
|
1912
|
+
DEFAULT_EXPIRES_IN_MS: 5 * 60 * 1e3,
|
|
1913
|
+
// 5 minutes
|
|
1914
|
+
DEFAULT_EXPIRES_IN_SECONDS: 5 * 60,
|
|
1915
|
+
REVOKE_REFRESH_TOKENS_ON_SIGNOUT: true
|
|
1916
|
+
};
|
|
1917
|
+
var DEFAULT_COOKIE_OPTIONS = {
|
|
1918
|
+
httpOnly: true,
|
|
1919
|
+
secure: process.env.NODE_ENV === "production",
|
|
1920
|
+
sameSite: "strict",
|
|
1921
|
+
path: "/"
|
|
1922
|
+
};
|
|
1428
1923
|
|
|
1429
|
-
// src/
|
|
1430
|
-
var
|
|
1431
|
-
var
|
|
1432
|
-
var
|
|
1433
|
-
var
|
|
1434
|
-
|
|
1924
|
+
// src/admin/nextSessionTernSecure.ts
|
|
1925
|
+
var import_cookie2 = require("@tern-secure/shared/cookie");
|
|
1926
|
+
var import_errors7 = require("@tern-secure/shared/errors");
|
|
1927
|
+
var import_headers = require("next/headers");
|
|
1928
|
+
var SESSION_CONSTANTS = {
|
|
1929
|
+
COOKIE_NAME: constants.Cookies.Session,
|
|
1930
|
+
DEFAULT_EXPIRES_IN_MS: 60 * 60 * 24 * 5 * 1e3,
|
|
1931
|
+
// 5 days
|
|
1932
|
+
DEFAULT_EXPIRES_IN_SECONDS: 60 * 60 * 24 * 5,
|
|
1933
|
+
REVOKE_REFRESH_TOKENS_ON_SIGNOUT: true
|
|
1934
|
+
};
|
|
1435
1935
|
|
|
1436
|
-
// src/
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1936
|
+
// src/admin/user.ts
|
|
1937
|
+
var import_errors8 = require("@tern-secure/shared/errors");
|
|
1938
|
+
|
|
1939
|
+
// src/app-check/verifier.ts
|
|
1940
|
+
var import_jose6 = require("jose");
|
|
1941
|
+
var getPublicKey = async (header, keyURL) => {
|
|
1942
|
+
const jswksUrl = new URL(keyURL);
|
|
1943
|
+
const getKey = (0, import_jose6.createRemoteJWKSet)(jswksUrl);
|
|
1944
|
+
return getKey(header);
|
|
1945
|
+
};
|
|
1946
|
+
var verifyAppCheckToken = async (token, options) => {
|
|
1947
|
+
const { data: decodedResult, errors } = ternDecodeJwt(token);
|
|
1948
|
+
if (errors) {
|
|
1949
|
+
throw errors[0];
|
|
1441
1950
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1951
|
+
const { header } = decodedResult;
|
|
1952
|
+
const { kid } = header;
|
|
1953
|
+
if (!kid) {
|
|
1954
|
+
return {
|
|
1955
|
+
errors: [
|
|
1956
|
+
new TokenVerificationError({
|
|
1957
|
+
reason: TokenVerificationErrorReason.TokenInvalid,
|
|
1958
|
+
message: 'JWT "kid" header is missing.'
|
|
1959
|
+
})
|
|
1960
|
+
]
|
|
1961
|
+
};
|
|
1445
1962
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1963
|
+
try {
|
|
1964
|
+
const getPublicKeyForToken = () => getPublicKey(header, options.keyURL || "");
|
|
1965
|
+
return await verifyAppCheckJwt(token, { ...options, key: getPublicKeyForToken });
|
|
1966
|
+
} catch (error) {
|
|
1967
|
+
if (error instanceof TokenVerificationError) {
|
|
1968
|
+
return { errors: [error] };
|
|
1969
|
+
}
|
|
1970
|
+
return {
|
|
1971
|
+
errors: [error]
|
|
1972
|
+
};
|
|
1455
1973
|
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
var accessTokenCache = /* @__PURE__ */ new Map();
|
|
1461
|
-
async function requestAccessToken(urlString, init) {
|
|
1462
|
-
const json = await fetchJson(urlString, init);
|
|
1463
|
-
if (!json.access_token || !json.expires_in) {
|
|
1464
|
-
throw new Error("Invalid access token response");
|
|
1974
|
+
};
|
|
1975
|
+
var AppcheckTokenVerifier = class {
|
|
1976
|
+
constructor(credential) {
|
|
1977
|
+
this.credential = credential;
|
|
1465
1978
|
}
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1979
|
+
verifyToken = async (token, projectId, options) => {
|
|
1980
|
+
const { data, errors } = await verifyAppCheckToken(token, options);
|
|
1981
|
+
if (errors) {
|
|
1982
|
+
throw errors[0];
|
|
1983
|
+
}
|
|
1984
|
+
return data;
|
|
1469
1985
|
};
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1986
|
+
};
|
|
1987
|
+
|
|
1988
|
+
// src/app-check/index.ts
|
|
1989
|
+
var JWKS_URL = "https://firebaseappcheck.googleapis.com/v1/jwks";
|
|
1990
|
+
var AppCheck = class {
|
|
1991
|
+
client;
|
|
1992
|
+
tokenGenerator;
|
|
1993
|
+
appCheckTokenVerifier;
|
|
1994
|
+
limitedUse;
|
|
1995
|
+
constructor(credential, tenantId, limitedUse) {
|
|
1996
|
+
this.client = new AppCheckApi2(credential);
|
|
1997
|
+
this.tokenGenerator = new AppCheckTokenGenerator(
|
|
1998
|
+
cryptoSignerFromCredential(credential, tenantId)
|
|
1999
|
+
);
|
|
2000
|
+
this.appCheckTokenVerifier = new AppcheckTokenVerifier(credential);
|
|
2001
|
+
this.limitedUse = limitedUse;
|
|
1479
2002
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
return requestAccessToken(url, {
|
|
1484
|
-
method: "POST",
|
|
1485
|
-
headers: {
|
|
1486
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
1487
|
-
Authorization: `Bearer ${token}`,
|
|
1488
|
-
Accept: "application/json"
|
|
1489
|
-
},
|
|
1490
|
-
body: postData
|
|
2003
|
+
createToken = (projectId, appId, options) => {
|
|
2004
|
+
return this.tokenGenerator.createCustomToken(appId, options).then((customToken) => {
|
|
2005
|
+
return this.client.exchangeToken({ customToken, projectId, appId });
|
|
1491
2006
|
});
|
|
1492
2007
|
};
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
const url = `https://${GOOGLE_AUTH_TOKEN_HOST}${GOOGLE_AUTH_TOKEN_PATH}`;
|
|
1500
|
-
if (refresh) {
|
|
1501
|
-
return this.fetchAndCacheAccessToken(url);
|
|
1502
|
-
}
|
|
1503
|
-
const cachedResponse = accessTokenCache.get(this.projectId);
|
|
1504
|
-
if (!cachedResponse || cachedResponse.expirationTime - Date.now() <= TOKEN_EXPIRY_THRESHOLD_MILLIS) {
|
|
1505
|
-
return this.fetchAndCacheAccessToken(url);
|
|
1506
|
-
}
|
|
1507
|
-
return cachedResponse;
|
|
1508
|
-
};
|
|
1509
|
-
createJwt = async () => {
|
|
1510
|
-
const iat = Math.floor(Date.now() / 1e3);
|
|
1511
|
-
const payload = {
|
|
1512
|
-
aud: GOOGLE_TOKEN_AUDIENCE,
|
|
1513
|
-
iat,
|
|
1514
|
-
exp: iat + ONE_HOUR_IN_SECONDS,
|
|
1515
|
-
iss: this.clientEmail,
|
|
1516
|
-
sub: this.clientEmail,
|
|
1517
|
-
scope: [
|
|
1518
|
-
"https://www.googleapis.com/auth/cloud-platform",
|
|
1519
|
-
"https://www.googleapis.com/auth/firebase.database",
|
|
1520
|
-
"https://www.googleapis.com/auth/firebase.messaging",
|
|
1521
|
-
"https://www.googleapis.com/auth/identitytoolkit",
|
|
1522
|
-
"https://www.googleapis.com/auth/userinfo.email"
|
|
1523
|
-
].join(" ")
|
|
1524
|
-
};
|
|
1525
|
-
return ternSignJwt({
|
|
1526
|
-
payload,
|
|
1527
|
-
privateKey: this.privateKey
|
|
2008
|
+
verifyToken = async (appCheckToken, projectId, options) => {
|
|
2009
|
+
return this.appCheckTokenVerifier.verifyToken(appCheckToken, projectId, { keyURL: JWKS_URL, ...options }).then((decodedToken) => {
|
|
2010
|
+
return {
|
|
2011
|
+
appId: decodedToken.app_id,
|
|
2012
|
+
token: decodedToken
|
|
2013
|
+
};
|
|
1528
2014
|
});
|
|
1529
2015
|
};
|
|
1530
2016
|
};
|
|
2017
|
+
function getAppCheck2(serviceAccount, tenantId, limitedUse) {
|
|
2018
|
+
return new AppCheck(new ServiceAccountManager(serviceAccount), tenantId, limitedUse);
|
|
2019
|
+
}
|
|
1531
2020
|
|
|
1532
2021
|
// src/auth/getauth.ts
|
|
1533
2022
|
var API_KEY_ERROR = "API Key is required";
|
|
@@ -1543,17 +2032,8 @@ function parseFirebaseResponse(data) {
|
|
|
1543
2032
|
return data;
|
|
1544
2033
|
}
|
|
1545
2034
|
function getAuth(options) {
|
|
1546
|
-
const { apiKey
|
|
1547
|
-
const
|
|
1548
|
-
const effectiveApiKey = apiKey || firebaseApiKey;
|
|
1549
|
-
let credential = null;
|
|
1550
|
-
if (firebaseAdminConfig?.projectId && firebaseAdminConfig?.privateKey && firebaseAdminConfig?.clientEmail) {
|
|
1551
|
-
credential = new ServiceAccountTokenManager({
|
|
1552
|
-
projectId: firebaseAdminConfig.projectId,
|
|
1553
|
-
privateKey: firebaseAdminConfig.privateKey,
|
|
1554
|
-
clientEmail: firebaseAdminConfig.clientEmail
|
|
1555
|
-
});
|
|
1556
|
-
}
|
|
2035
|
+
const { apiKey } = options;
|
|
2036
|
+
const effectiveApiKey = apiKey || process.env.NEXT_PUBLIC_FIREBASE_API_KEY;
|
|
1557
2037
|
async function getUserData(idToken, localId) {
|
|
1558
2038
|
if (!effectiveApiKey) {
|
|
1559
2039
|
throw new Error(API_KEY_ERROR);
|
|
@@ -1638,43 +2118,12 @@ function getAuth(options) {
|
|
|
1638
2118
|
auth_time: decodedCustomIdToken.data.auth_time
|
|
1639
2119
|
};
|
|
1640
2120
|
}
|
|
1641
|
-
async function
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
error: new Error(
|
|
1646
|
-
"Firebase Admin config must be provided to exchange App Check tokens."
|
|
1647
|
-
)
|
|
1648
|
-
};
|
|
1649
|
-
}
|
|
1650
|
-
if (!effectiveApiKey) {
|
|
1651
|
-
return { data: null, error: new Error(API_KEY_ERROR) };
|
|
1652
|
-
}
|
|
2121
|
+
async function createAppCheckToken() {
|
|
2122
|
+
const adminConfig = loadAdminConfig();
|
|
2123
|
+
const appId = process.env.NEXT_PUBLIC_FIREBASE_APP_ID || "";
|
|
2124
|
+
const appCheck = getAppCheck2(adminConfig, options.tenantId);
|
|
1653
2125
|
try {
|
|
1654
|
-
const
|
|
1655
|
-
if (decoded.errors) {
|
|
1656
|
-
return { data: null, error: decoded.errors[0] };
|
|
1657
|
-
}
|
|
1658
|
-
const customToken = await createCustomToken(decoded.data.uid, {
|
|
1659
|
-
emailVerified: decoded.data.email_verified,
|
|
1660
|
-
source_sign_in_provider: decoded.data.firebase.sign_in_provider
|
|
1661
|
-
});
|
|
1662
|
-
const projectId = options.firebaseConfig?.projectId;
|
|
1663
|
-
const appId = options.firebaseConfig?.appId;
|
|
1664
|
-
if (!projectId || !appId) {
|
|
1665
|
-
return { data: null, error: new Error("Project ID and App ID are required for App Check") };
|
|
1666
|
-
}
|
|
1667
|
-
const { accessToken } = await credential.getAccessToken();
|
|
1668
|
-
const appCheckResponse = await options.apiClient?.appCheck.exchangeCustomToken({
|
|
1669
|
-
accessToken,
|
|
1670
|
-
projectId,
|
|
1671
|
-
appId,
|
|
1672
|
-
customToken,
|
|
1673
|
-
limitedUse: false
|
|
1674
|
-
});
|
|
1675
|
-
if (!appCheckResponse?.token) {
|
|
1676
|
-
return { data: null, error: new Error("Failed to exchange for App Check token") };
|
|
1677
|
-
}
|
|
2126
|
+
const appCheckResponse = await appCheck.createToken(adminConfig.projectId, appId);
|
|
1678
2127
|
return {
|
|
1679
2128
|
data: {
|
|
1680
2129
|
token: appCheckResponse.token,
|
|
@@ -1686,93 +2135,104 @@ function getAuth(options) {
|
|
|
1686
2135
|
return { data: null, error };
|
|
1687
2136
|
}
|
|
1688
2137
|
}
|
|
2138
|
+
async function verifyAppCheckToken2(token) {
|
|
2139
|
+
const adminConfig = loadAdminConfig();
|
|
2140
|
+
const appCheck = getAppCheck2(adminConfig, options.tenantId);
|
|
2141
|
+
try {
|
|
2142
|
+
const decodedToken = await appCheck.verifyToken(token, adminConfig.projectId, {});
|
|
2143
|
+
return {
|
|
2144
|
+
data: decodedToken,
|
|
2145
|
+
error: null
|
|
2146
|
+
};
|
|
2147
|
+
} catch (error) {
|
|
2148
|
+
return { data: null, error };
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
1689
2151
|
return {
|
|
1690
2152
|
getUserData,
|
|
1691
2153
|
customForIdAndRefreshToken,
|
|
1692
2154
|
createCustomIdAndRefreshToken,
|
|
1693
2155
|
refreshExpiredIdToken,
|
|
1694
|
-
|
|
2156
|
+
createAppCheckToken,
|
|
2157
|
+
verifyAppCheckToken: verifyAppCheckToken2
|
|
1695
2158
|
};
|
|
1696
2159
|
}
|
|
1697
2160
|
|
|
1698
|
-
// src/
|
|
1699
|
-
var
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
this.initCookieValues();
|
|
1705
|
-
this.initHandshakeValues();
|
|
1706
|
-
this.initUrlValues();
|
|
1707
|
-
Object.assign(this, options);
|
|
1708
|
-
this.ternUrl = this.ternSecureRequest.ternUrl;
|
|
1709
|
-
}
|
|
1710
|
-
get request() {
|
|
1711
|
-
return this.ternSecureRequest;
|
|
1712
|
-
}
|
|
1713
|
-
initHeaderValues() {
|
|
1714
|
-
this.sessionTokenInHeader = this.parseAuthorizationHeader(
|
|
1715
|
-
this.getHeader(constants.Headers.Authorization)
|
|
1716
|
-
);
|
|
1717
|
-
this.origin = this.getHeader(constants.Headers.Origin);
|
|
1718
|
-
this.host = this.getHeader(constants.Headers.Host);
|
|
1719
|
-
this.forwardedHost = this.getHeader(constants.Headers.ForwardedHost);
|
|
1720
|
-
this.forwardedProto = this.getHeader(constants.Headers.CloudFrontForwardedProto) || this.getHeader(constants.Headers.ForwardedProto);
|
|
1721
|
-
this.referrer = this.getHeader(constants.Headers.Referrer);
|
|
1722
|
-
this.userAgent = this.getHeader(constants.Headers.UserAgent);
|
|
1723
|
-
this.secFetchDest = this.getHeader(constants.Headers.SecFetchDest);
|
|
1724
|
-
this.accept = this.getHeader(constants.Headers.Accept);
|
|
1725
|
-
this.appCheckToken = this.getHeader(constants.Headers.AppCheckToken);
|
|
1726
|
-
}
|
|
1727
|
-
initCookieValues() {
|
|
1728
|
-
const isProduction = process.env.NODE_ENV === "production";
|
|
1729
|
-
const defaultPrefix = isProduction ? "__HOST-" : "__dev_";
|
|
1730
|
-
this.sessionTokenInCookie = this.getCookie(constants.Cookies.Session);
|
|
1731
|
-
this.idTokenInCookie = this.getCookie(`${defaultPrefix}${constants.Cookies.IdToken}`);
|
|
1732
|
-
this.refreshTokenInCookie = this.getCookie(`${defaultPrefix}${constants.Cookies.Refresh}`);
|
|
1733
|
-
this.csrfTokenInCookie = this.getCookie(constants.Cookies.CsrfToken);
|
|
1734
|
-
this.customTokenInCookie = this.getCookie(constants.Cookies.Custom);
|
|
1735
|
-
this.ternAuth = Number.parseInt(this.getCookie(constants.Cookies.TernAut) || "0", 10);
|
|
1736
|
-
}
|
|
1737
|
-
initHandshakeValues() {
|
|
1738
|
-
this.handshakeToken = this.getQueryParam(constants.QueryParameters.Handshake) || this.getCookie(constants.Cookies.Handshake);
|
|
1739
|
-
this.handshakeNonce = this.getQueryParam(constants.QueryParameters.HandshakeNonce) || this.getCookie(constants.Cookies.HandshakeNonce);
|
|
1740
|
-
}
|
|
1741
|
-
initUrlValues() {
|
|
1742
|
-
this.method = this.ternSecureRequest.method;
|
|
1743
|
-
this.pathSegments = this.ternSecureRequest.ternUrl.pathname.split("/").filter(Boolean);
|
|
1744
|
-
this.endpoint = this.pathSegments[2];
|
|
1745
|
-
this.subEndpoint = this.pathSegments[3];
|
|
1746
|
-
}
|
|
1747
|
-
getQueryParam(name) {
|
|
1748
|
-
return this.ternSecureRequest.ternUrl.searchParams.get(name);
|
|
1749
|
-
}
|
|
1750
|
-
getHeader(name) {
|
|
1751
|
-
return this.ternSecureRequest.headers.get(name) || void 0;
|
|
2161
|
+
// src/auth/credential.ts
|
|
2162
|
+
var accessTokenCache = /* @__PURE__ */ new Map();
|
|
2163
|
+
async function requestAccessToken(urlString, init) {
|
|
2164
|
+
const json = await fetchJson(urlString, init);
|
|
2165
|
+
if (!json.access_token || !json.expires_in) {
|
|
2166
|
+
throw new Error("Invalid access token response");
|
|
1752
2167
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
2168
|
+
return {
|
|
2169
|
+
accessToken: json.access_token,
|
|
2170
|
+
expirationTime: Date.now() + json.expires_in * 1e3
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
var ServiceAccountManager = class {
|
|
2174
|
+
projectId;
|
|
2175
|
+
privateKey;
|
|
2176
|
+
clientEmail;
|
|
2177
|
+
constructor(serviceAccount) {
|
|
2178
|
+
this.projectId = serviceAccount.projectId;
|
|
2179
|
+
this.privateKey = serviceAccount.privateKey;
|
|
2180
|
+
this.clientEmail = serviceAccount.clientEmail;
|
|
1755
2181
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
2182
|
+
fetchAccessToken = async (url) => {
|
|
2183
|
+
const token = await this.createJwt();
|
|
2184
|
+
const postData = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" + token;
|
|
2185
|
+
return requestAccessToken(url, {
|
|
2186
|
+
method: "POST",
|
|
2187
|
+
headers: {
|
|
2188
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
2189
|
+
Authorization: `Bearer ${token}`,
|
|
2190
|
+
Accept: "application/json"
|
|
2191
|
+
},
|
|
2192
|
+
body: postData
|
|
2193
|
+
});
|
|
2194
|
+
};
|
|
2195
|
+
fetchAndCacheAccessToken = async (url) => {
|
|
2196
|
+
const accessToken = await this.fetchAccessToken(url);
|
|
2197
|
+
accessTokenCache.set(this.projectId, accessToken);
|
|
2198
|
+
return accessToken;
|
|
2199
|
+
};
|
|
2200
|
+
getAccessToken = async (refresh) => {
|
|
2201
|
+
const url = `https://${GOOGLE_AUTH_TOKEN_HOST}${GOOGLE_AUTH_TOKEN_PATH}`;
|
|
2202
|
+
if (refresh) {
|
|
2203
|
+
return this.fetchAndCacheAccessToken(url);
|
|
1763
2204
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
2205
|
+
const cachedResponse = accessTokenCache.get(this.projectId);
|
|
2206
|
+
if (!cachedResponse || cachedResponse.expirationTime - Date.now() <= TOKEN_EXPIRY_THRESHOLD_MILLIS) {
|
|
2207
|
+
return this.fetchAndCacheAccessToken(url);
|
|
1766
2208
|
}
|
|
1767
|
-
return
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
2209
|
+
return cachedResponse;
|
|
2210
|
+
};
|
|
2211
|
+
createJwt = async () => {
|
|
2212
|
+
const iat = Math.floor(Date.now() / 1e3);
|
|
2213
|
+
const payload = {
|
|
2214
|
+
aud: GOOGLE_TOKEN_AUDIENCE,
|
|
2215
|
+
iat,
|
|
2216
|
+
exp: iat + ONE_HOUR_IN_SECONDS,
|
|
2217
|
+
iss: this.clientEmail,
|
|
2218
|
+
sub: this.clientEmail,
|
|
2219
|
+
scope: [
|
|
2220
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
2221
|
+
"https://www.googleapis.com/auth/firebase.database",
|
|
2222
|
+
"https://www.googleapis.com/auth/firebase.messaging",
|
|
2223
|
+
"https://www.googleapis.com/auth/identitytoolkit",
|
|
2224
|
+
"https://www.googleapis.com/auth/userinfo.email"
|
|
2225
|
+
].join(" ")
|
|
2226
|
+
};
|
|
2227
|
+
return ternSignJwt({
|
|
2228
|
+
payload,
|
|
2229
|
+
privateKey: this.privateKey
|
|
2230
|
+
});
|
|
2231
|
+
};
|
|
1772
2232
|
};
|
|
1773
2233
|
|
|
1774
2234
|
// src/tokens/cookie.ts
|
|
1775
|
-
var
|
|
2235
|
+
var import_cookie3 = require("@tern-secure/shared/cookie");
|
|
1776
2236
|
|
|
1777
2237
|
// src/tokens/request.ts
|
|
1778
2238
|
function hasAuthorizationHeader(request) {
|
|
@@ -1784,9 +2244,9 @@ function convertToSeconds(value) {
|
|
|
1784
2244
|
function isRequestForRefresh(error, context, request) {
|
|
1785
2245
|
return error.reason === TokenVerificationErrorReason.TokenExpired && !!context.refreshTokenInCookie && request.method === "GET";
|
|
1786
2246
|
}
|
|
1787
|
-
async function
|
|
2247
|
+
async function authenticateRequest2(request, options) {
|
|
1788
2248
|
const context = createRequestProcessor(createTernSecureRequest(request), options);
|
|
1789
|
-
const { refreshTokenInCookie
|
|
2249
|
+
const { refreshTokenInCookie } = context;
|
|
1790
2250
|
const { refreshExpiredIdToken } = getAuth(options);
|
|
1791
2251
|
function checkSessionTimeout(authTimeValue) {
|
|
1792
2252
|
const defaultMaxAgeSeconds = convertToSeconds("5 days");
|
|
@@ -1809,8 +2269,7 @@ async function authenticateRequest(request, options) {
|
|
|
1809
2269
|
};
|
|
1810
2270
|
}
|
|
1811
2271
|
return await refreshExpiredIdToken(refreshTokenInCookie, {
|
|
1812
|
-
referer: context.ternUrl.origin
|
|
1813
|
-
appCheckToken
|
|
2272
|
+
referer: context.ternUrl.origin
|
|
1814
2273
|
});
|
|
1815
2274
|
}
|
|
1816
2275
|
async function handleRefresh() {
|
|
@@ -1821,8 +2280,8 @@ async function authenticateRequest(request, options) {
|
|
|
1821
2280
|
const headers = new Headers();
|
|
1822
2281
|
const { idToken } = refreshedData;
|
|
1823
2282
|
const maxAge = 365 * 24 * 60 * 60;
|
|
1824
|
-
const cookiePrefix = (0,
|
|
1825
|
-
const idTokenCookieName = (0,
|
|
2283
|
+
const cookiePrefix = (0, import_cookie3.getCookiePrefix)();
|
|
2284
|
+
const idTokenCookieName = (0, import_cookie3.getCookieName)(constants.Cookies.IdToken, cookiePrefix);
|
|
1826
2285
|
const baseCookieAttributes = `HttpOnly; Secure; SameSite=Strict; Max-Age=${maxAge}; Path=/`;
|
|
1827
2286
|
const idTokenCookie = `${idTokenCookieName}=${idToken}; ${baseCookieAttributes};`;
|
|
1828
2287
|
headers.append("Set-Cookie", idTokenCookie);
|
|
@@ -1912,24 +2371,7 @@ async function authenticateRequest(request, options) {
|
|
|
1912
2371
|
if (errors) {
|
|
1913
2372
|
throw errors[0];
|
|
1914
2373
|
}
|
|
1915
|
-
const
|
|
1916
|
-
let appCheckTokenValue;
|
|
1917
|
-
try {
|
|
1918
|
-
const idToken = context.idTokenInCookie || "";
|
|
1919
|
-
const appCheckResult = await exchangeAppCheckToken(idToken);
|
|
1920
|
-
console.log("[authenticateRequest] App Check exchange result:", appCheckResult);
|
|
1921
|
-
if (appCheckResult.data?.token) {
|
|
1922
|
-
appCheckTokenValue = appCheckResult.data.token;
|
|
1923
|
-
}
|
|
1924
|
-
} catch (error) {
|
|
1925
|
-
console.warn("App Check token exchange failed:", error);
|
|
1926
|
-
}
|
|
1927
|
-
const headers = new Headers();
|
|
1928
|
-
headers.set(
|
|
1929
|
-
constants.Headers.AppCheckToken,
|
|
1930
|
-
appCheckTokenValue || ""
|
|
1931
|
-
);
|
|
1932
|
-
const signedInRequestState = signedIn(context, data, headers, context.idTokenInCookie);
|
|
2374
|
+
const signedInRequestState = signedIn(context, data, void 0, context.idTokenInCookie);
|
|
1933
2375
|
return signedInRequestState;
|
|
1934
2376
|
} catch (err) {
|
|
1935
2377
|
return handleError(err, "cookie");
|
|
@@ -1943,23 +2385,7 @@ async function authenticateRequest(request, options) {
|
|
|
1943
2385
|
if (errors) {
|
|
1944
2386
|
throw errors[0];
|
|
1945
2387
|
}
|
|
1946
|
-
const
|
|
1947
|
-
let appCheckTokenValue;
|
|
1948
|
-
try {
|
|
1949
|
-
const token = sessionTokenInHeader || "";
|
|
1950
|
-
const appCheckResult = await exchangeAppCheckToken(token);
|
|
1951
|
-
if (appCheckResult.data?.token) {
|
|
1952
|
-
appCheckTokenValue = appCheckResult.data.token;
|
|
1953
|
-
}
|
|
1954
|
-
} catch (error) {
|
|
1955
|
-
console.warn("App Check token exchange failed:", error);
|
|
1956
|
-
}
|
|
1957
|
-
const headers = new Headers();
|
|
1958
|
-
headers.set(
|
|
1959
|
-
constants.Headers.AppCheckToken,
|
|
1960
|
-
appCheckTokenValue || ""
|
|
1961
|
-
);
|
|
1962
|
-
const signedInRequestState = signedIn(context, data, headers, sessionTokenInHeader);
|
|
2388
|
+
const signedInRequestState = signedIn(context, data, void 0, sessionTokenInHeader);
|
|
1963
2389
|
return signedInRequestState;
|
|
1964
2390
|
} catch (err) {
|
|
1965
2391
|
return handleError(err, "header");
|
|
@@ -1973,17 +2399,8 @@ async function authenticateRequest(request, options) {
|
|
|
1973
2399
|
if (isRequestForRefresh(err, context, request)) {
|
|
1974
2400
|
const { data, error } = await handleRefresh();
|
|
1975
2401
|
if (data) {
|
|
1976
|
-
const
|
|
1977
|
-
|
|
1978
|
-
try {
|
|
1979
|
-
const appCheckResult = await exchangeAppCheckToken(data.token);
|
|
1980
|
-
if (appCheckResult.data?.token) {
|
|
1981
|
-
appCheckTokenValue = appCheckResult.data.token;
|
|
1982
|
-
}
|
|
1983
|
-
} catch (error2) {
|
|
1984
|
-
console.warn("App Check token exchange failed in error handler:", error2);
|
|
1985
|
-
}
|
|
1986
|
-
return signedIn(context, data.decoded, data.headers, data.token);
|
|
2402
|
+
const signedInState = signedIn(context, data.decoded, data.headers, data.token);
|
|
2403
|
+
return signedInState;
|
|
1987
2404
|
}
|
|
1988
2405
|
if (error?.cause?.reason) {
|
|
1989
2406
|
refreshError = error.cause.reason;
|
|
@@ -2012,7 +2429,7 @@ function createAuthenticateRequest(params) {
|
|
|
2012
2429
|
const apiClient = params.apiClient;
|
|
2013
2430
|
const handleAuthenticateRequest = (request, options = {}) => {
|
|
2014
2431
|
const { apiUrl } = buildTimeOptions;
|
|
2015
|
-
return
|
|
2432
|
+
return authenticateRequest2(request, { ...options, apiUrl, apiClient });
|
|
2016
2433
|
};
|
|
2017
2434
|
return {
|
|
2018
2435
|
authenticateRequest: handleAuthenticateRequest
|
|
@@ -2161,7 +2578,7 @@ var PostgresAdapter = class {
|
|
|
2161
2578
|
};
|
|
2162
2579
|
|
|
2163
2580
|
// src/adapters/RedisAdapter.ts
|
|
2164
|
-
var
|
|
2581
|
+
var import_redis2 = require("@upstash/redis");
|
|
2165
2582
|
var TTLCache = class {
|
|
2166
2583
|
cache = /* @__PURE__ */ new Map();
|
|
2167
2584
|
defaultTTL;
|
|
@@ -2218,7 +2635,7 @@ var RedisAdapter = class {
|
|
|
2218
2635
|
cache;
|
|
2219
2636
|
keyPrefix;
|
|
2220
2637
|
constructor(config) {
|
|
2221
|
-
this.redis = new
|
|
2638
|
+
this.redis = new import_redis2.Redis({
|
|
2222
2639
|
url: config.url,
|
|
2223
2640
|
token: config.token
|
|
2224
2641
|
});
|
|
@@ -2300,6 +2717,7 @@ function validateCheckRevokedOptions(options) {
|
|
|
2300
2717
|
createAdapter,
|
|
2301
2718
|
createBackendInstanceClient,
|
|
2302
2719
|
createRedirect,
|
|
2720
|
+
createRequestProcessor,
|
|
2303
2721
|
createTernSecureRequest,
|
|
2304
2722
|
disableDebugLogging,
|
|
2305
2723
|
enableDebugLogging,
|
|
@@ -2307,6 +2725,7 @@ function validateCheckRevokedOptions(options) {
|
|
|
2307
2725
|
signedIn,
|
|
2308
2726
|
signedInAuthObject,
|
|
2309
2727
|
signedOutAuthObject,
|
|
2310
|
-
validateCheckRevokedOptions
|
|
2728
|
+
validateCheckRevokedOptions,
|
|
2729
|
+
verifyToken
|
|
2311
2730
|
});
|
|
2312
2731
|
//# sourceMappingURL=index.js.map
|