firebase-functions 3.19.0 → 3.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/lib/apps.js +1 -1
  2. package/lib/bin/firebase-functions.js +1 -1
  3. package/lib/cloud-functions.js +15 -18
  4. package/lib/common/providers/https.d.ts +20 -49
  5. package/lib/common/providers/https.js +10 -50
  6. package/lib/common/providers/identity.d.ts +187 -4
  7. package/lib/common/providers/identity.js +553 -27
  8. package/lib/common/providers/tasks.d.ts +58 -0
  9. package/lib/common/providers/tasks.js +68 -0
  10. package/lib/function-builder.d.ts +4 -1
  11. package/lib/function-builder.js +6 -1
  12. package/lib/handler-builder.d.ts +13 -2
  13. package/lib/handler-builder.js +18 -5
  14. package/lib/index.d.ts +2 -1
  15. package/lib/index.js +5 -7
  16. package/lib/logger/compat.js +1 -1
  17. package/lib/providers/analytics.js +1 -1
  18. package/lib/providers/auth.js +2 -2
  19. package/lib/providers/database.js +11 -11
  20. package/lib/providers/firestore.js +7 -7
  21. package/lib/providers/https.d.ts +2 -44
  22. package/lib/providers/https.js +8 -56
  23. package/lib/providers/pubsub.js +2 -2
  24. package/lib/providers/remoteConfig.js +1 -1
  25. package/lib/providers/storage.js +2 -2
  26. package/lib/providers/tasks.d.ts +46 -0
  27. package/lib/providers/tasks.js +75 -0
  28. package/lib/providers/testLab.js +1 -1
  29. package/lib/runtime/manifest.d.ts +3 -10
  30. package/lib/runtime/manifest.js +21 -0
  31. package/lib/setup.js +3 -3
  32. package/lib/v2/index.d.ts +2 -1
  33. package/lib/v2/index.js +3 -1
  34. package/lib/v2/options.js +11 -11
  35. package/lib/v2/providers/alerts/alerts.js +4 -10
  36. package/lib/v2/providers/alerts/appDistribution.js +1 -1
  37. package/lib/v2/providers/alerts/billing.js +1 -1
  38. package/lib/v2/providers/alerts/crashlytics.js +1 -1
  39. package/lib/v2/providers/alerts/index.js +1 -5
  40. package/lib/v2/providers/https.d.ts +2 -20
  41. package/lib/v2/providers/https.js +4 -43
  42. package/lib/v2/providers/pubsub.js +2 -2
  43. package/lib/v2/providers/storage.js +3 -3
  44. package/lib/v2/providers/tasks.d.ts +22 -0
  45. package/lib/v2/providers/tasks.js +89 -0
  46. package/package.json +23 -13
@@ -21,8 +21,45 @@
21
21
  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
22
  // SOFTWARE.
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.userRecordConstructor = exports.UserRecordMetadata = void 0;
25
- const _ = require("lodash");
24
+ exports.createHandler = exports.getUpdateMask = exports.validateAuthResponse = exports.parseAuthEventContext = exports.parseAuthUserRecord = exports.parseMultiFactor = exports.parseDate = exports.parseProviderData = exports.parseMetadata = exports.verifyJWT = exports.shouldVerifyJWT = exports.decodeJWT = exports.checkDecodedToken = exports.isAuthorizedCloudFunctionURL = exports.getPublicKeyFromHeader = exports.isValidRequest = exports.setKeyExpirationTime = exports.invalidPublicKeys = exports.userRecordConstructor = exports.UserRecordMetadata = exports.JWT_ISSUER = exports.JWT_ALG = exports.JWT_CLIENT_CERT_PATH = exports.JWT_CLIENT_CERT_URL = exports.INVALID_TOKEN_BUFFER = exports.HttpsError = void 0;
25
+ const jwt = require("jsonwebtoken");
26
+ const node_fetch_1 = require("node-fetch");
27
+ const https_1 = require("./https");
28
+ Object.defineProperty(exports, "HttpsError", { enumerable: true, get: function () { return https_1.HttpsError; } });
29
+ const function_configuration_1 = require("../../function-configuration");
30
+ const __1 = require("../..");
31
+ /** @internal */
32
+ exports.INVALID_TOKEN_BUFFER = 60000; // set to 1 minute
33
+ /** @internal */
34
+ exports.JWT_CLIENT_CERT_URL = 'https://www.googleapis.com';
35
+ /** @internal */
36
+ exports.JWT_CLIENT_CERT_PATH = 'robot/v1/metadata/x509/securetoken@system.gserviceaccount.com';
37
+ /** @internal */
38
+ exports.JWT_ALG = 'RS256';
39
+ /** @internal */
40
+ exports.JWT_ISSUER = 'https://securetoken.google.com/';
41
+ const DISALLOWED_CUSTOM_CLAIMS = [
42
+ 'acr',
43
+ 'amr',
44
+ 'at_hash',
45
+ 'aud',
46
+ 'auth_time',
47
+ 'azp',
48
+ 'cnf',
49
+ 'c_hash',
50
+ 'exp',
51
+ 'iat',
52
+ 'iss',
53
+ 'jti',
54
+ 'nbf',
55
+ 'nonce',
56
+ 'firebase',
57
+ ];
58
+ const CLAIMS_MAX_PAYLOAD_SIZE = 1000;
59
+ const EVENT_MAPPING = {
60
+ beforeCreate: 'providers/cloud.auth/eventTypes/user.beforeCreate',
61
+ beforeSignIn: 'providers/cloud.auth/eventTypes/user.beforeSignIn',
62
+ };
26
63
  /**
27
64
  * Helper class to create the user metadata in a UserRecord object
28
65
  */
@@ -60,37 +97,526 @@ function userRecordConstructor(wireData) {
60
97
  passwordHash: null,
61
98
  tokensValidAfterTime: null,
62
99
  };
63
- const record = _.assign({}, falseyValues, wireData);
64
- const meta = _.get(record, 'metadata');
100
+ const record = { ...falseyValues, ...wireData };
101
+ const meta = record['metadata'];
65
102
  if (meta) {
66
- _.set(record, 'metadata', new UserRecordMetadata(meta.createdAt || meta.creationTime, meta.lastSignedInAt || meta.lastSignInTime));
103
+ record['metadata'] = new UserRecordMetadata(meta.createdAt || meta.creationTime, meta.lastSignedInAt || meta.lastSignInTime);
67
104
  }
68
105
  else {
69
- _.set(record, 'metadata', new UserRecordMetadata(null, null));
106
+ record['metadata'] = new UserRecordMetadata(null, null);
70
107
  }
71
- _.forEach(record.providerData, (entry) => {
72
- _.set(entry, 'toJSON', () => {
108
+ for (const entry of Object.entries(record.providerData)) {
109
+ entry['toJSON'] = () => {
73
110
  return entry;
74
- });
75
- });
76
- _.set(record, 'toJSON', () => {
77
- const json = _.pick(record, [
78
- 'uid',
79
- 'email',
80
- 'emailVerified',
81
- 'displayName',
82
- 'photoURL',
83
- 'phoneNumber',
84
- 'disabled',
85
- 'passwordHash',
86
- 'passwordSalt',
87
- 'tokensValidAfterTime',
88
- ]);
89
- json.metadata = _.get(record, 'metadata').toJSON();
90
- json.customClaims = _.cloneDeep(record.customClaims);
91
- json.providerData = _.map(record.providerData, (entry) => entry.toJSON());
111
+ };
112
+ }
113
+ record['toJSON'] = () => {
114
+ const { uid, email, emailVerified, displayName, photoURL, phoneNumber, disabled, passwordHash, passwordSalt, tokensValidAfterTime, } = record;
115
+ const json = {
116
+ uid,
117
+ email,
118
+ emailVerified,
119
+ displayName,
120
+ photoURL,
121
+ phoneNumber,
122
+ disabled,
123
+ passwordHash,
124
+ passwordSalt,
125
+ tokensValidAfterTime,
126
+ };
127
+ json['metadata'] = record['metadata'].toJSON();
128
+ json['customClaims'] = JSON.parse(JSON.stringify(record.customClaims));
129
+ json['providerData'] = record.providerData.map((entry) => entry.toJSON());
92
130
  return json;
93
- });
131
+ };
94
132
  return record;
95
133
  }
96
134
  exports.userRecordConstructor = userRecordConstructor;
135
+ /**
136
+ * Helper to determine if we refresh the public keys
137
+ * @internal
138
+ */
139
+ function invalidPublicKeys(keys, time = Date.now()) {
140
+ if (!keys.publicKeysExpireAt) {
141
+ return true;
142
+ }
143
+ return time + exports.INVALID_TOKEN_BUFFER >= keys.publicKeysExpireAt;
144
+ }
145
+ exports.invalidPublicKeys = invalidPublicKeys;
146
+ /**
147
+ * Helper to parse the response headers to obtain the expiration time.
148
+ * @internal
149
+ */
150
+ function setKeyExpirationTime(response, keysCache, time) {
151
+ if (response.headers.has('cache-control')) {
152
+ const ccHeader = response.headers.get('cache-control');
153
+ const maxAgeEntry = ccHeader
154
+ .split(', ')
155
+ .find((item) => item.includes('max-age'));
156
+ if (maxAgeEntry) {
157
+ const maxAge = +maxAgeEntry.trim().split('=')[1];
158
+ keysCache.publicKeysExpireAt = time + maxAge * 1000;
159
+ }
160
+ }
161
+ }
162
+ exports.setKeyExpirationTime = setKeyExpirationTime;
163
+ /**
164
+ * Fetch the public keys for use in decoding and verifying the jwt sent from identity platform.
165
+ */
166
+ async function refreshPublicKeys(keysCache, time = Date.now()) {
167
+ const url = `${exports.JWT_CLIENT_CERT_URL}/${exports.JWT_CLIENT_CERT_PATH}`;
168
+ try {
169
+ const response = await node_fetch_1.default(url);
170
+ setKeyExpirationTime(response, keysCache, time);
171
+ const data = await response.json();
172
+ keysCache.publicKeys = data;
173
+ }
174
+ catch (err) {
175
+ __1.logger.error(`Failed to obtain public keys for JWT verification: ${err.message}`);
176
+ throw new https_1.HttpsError('internal', 'Failed to obtain the public keys for JWT verification.');
177
+ }
178
+ }
179
+ /**
180
+ * Checks for a valid identity platform web request, otherwise throws an HttpsError
181
+ * @internal
182
+ */
183
+ function isValidRequest(req) {
184
+ var _a, _b;
185
+ if (req.method !== 'POST') {
186
+ __1.logger.warn(`Request has invalid method "${req.method}".`);
187
+ return false;
188
+ }
189
+ const contentType = (req.header('Content-Type') || '').toLowerCase();
190
+ if (!contentType.includes('application/json')) {
191
+ __1.logger.warn('Request has invalid header Content-Type.');
192
+ return false;
193
+ }
194
+ if (!((_b = (_a = req.body) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.jwt)) {
195
+ __1.logger.warn('Request has an invalid body.');
196
+ return false;
197
+ }
198
+ return true;
199
+ }
200
+ exports.isValidRequest = isValidRequest;
201
+ /** @internal */
202
+ function getPublicKeyFromHeader(header, publicKeys) {
203
+ if (header.alg !== exports.JWT_ALG) {
204
+ throw new https_1.HttpsError('invalid-argument', `Provided JWT has incorrect algorithm. Expected ${exports.JWT_ALG} but got ${header.alg}.`);
205
+ }
206
+ if (!header.kid) {
207
+ throw new https_1.HttpsError('invalid-argument', 'JWT header missing "kid" claim.');
208
+ }
209
+ if (!publicKeys.hasOwnProperty(header.kid)) {
210
+ throw new https_1.HttpsError('invalid-argument', 'Provided JWT has "kid" claim which does not correspond to a known public key. Most likely the JWT is expired.');
211
+ }
212
+ return publicKeys[header.kid];
213
+ }
214
+ exports.getPublicKeyFromHeader = getPublicKeyFromHeader;
215
+ /**
216
+ * Checks for a well forms cloud functions url
217
+ * @internal
218
+ */
219
+ function isAuthorizedCloudFunctionURL(cloudFunctionUrl, projectId) {
220
+ const re = new RegExp(`^https://(${function_configuration_1.SUPPORTED_REGIONS.join('|')})+-${projectId}\.cloudfunctions\.net/`);
221
+ const res = re.exec(cloudFunctionUrl) || [];
222
+ return res.length > 0;
223
+ }
224
+ exports.isAuthorizedCloudFunctionURL = isAuthorizedCloudFunctionURL;
225
+ /**
226
+ * Checks for errors in a decoded jwt
227
+ * @internal
228
+ */
229
+ function checkDecodedToken(decodedJWT, eventType, projectId) {
230
+ if (decodedJWT.event_type !== eventType) {
231
+ throw new https_1.HttpsError('invalid-argument', `Expected "${eventType}" but received "${decodedJWT.event_type}".`);
232
+ }
233
+ if (!isAuthorizedCloudFunctionURL(decodedJWT.aud, projectId)) {
234
+ throw new https_1.HttpsError('invalid-argument', 'Provided JWT has incorrect "aud" (audience) claim.');
235
+ }
236
+ if (decodedJWT.iss !== `${exports.JWT_ISSUER}${projectId}`) {
237
+ throw new https_1.HttpsError('invalid-argument', `Provided JWT has incorrect "iss" (issuer) claim. Expected ` +
238
+ `"${exports.JWT_ISSUER}${projectId}" but got "${decodedJWT.iss}".`);
239
+ }
240
+ if (typeof decodedJWT.sub !== 'string' || decodedJWT.sub.length === 0) {
241
+ throw new https_1.HttpsError('invalid-argument', 'Provided JWT has no "sub" (subject) claim.');
242
+ }
243
+ if (decodedJWT.sub.length > 128) {
244
+ throw new https_1.HttpsError('invalid-argument', 'Provided JWT has "sub" (subject) claim longer than 128 characters.');
245
+ }
246
+ // set uid to sub
247
+ decodedJWT.uid = decodedJWT.sub;
248
+ }
249
+ exports.checkDecodedToken = checkDecodedToken;
250
+ /**
251
+ * Helper function to decode the jwt, internally uses the 'jsonwebtoken' package.
252
+ * @internal
253
+ */
254
+ function decodeJWT(token) {
255
+ let decoded;
256
+ try {
257
+ decoded = jwt.decode(token, { complete: true });
258
+ }
259
+ catch (err) {
260
+ __1.logger.error('Decoding the JWT failed', err);
261
+ throw new https_1.HttpsError('internal', 'Failed to decode the JWT.');
262
+ }
263
+ if (!(decoded === null || decoded === void 0 ? void 0 : decoded.payload)) {
264
+ throw new https_1.HttpsError('internal', 'The decoded JWT is not structured correctly.');
265
+ }
266
+ return decoded;
267
+ }
268
+ exports.decodeJWT = decodeJWT;
269
+ /**
270
+ * Helper function to determine if we need to do full verification of the jwt
271
+ * @internal
272
+ */
273
+ function shouldVerifyJWT() {
274
+ // TODO(colerogers): add emulator support to skip verification
275
+ return true;
276
+ }
277
+ exports.shouldVerifyJWT = shouldVerifyJWT;
278
+ /**
279
+ * Verifies the jwt using the 'jwt' library and decodes the token with the public keys
280
+ * Throws an error if the event types do not match
281
+ * @internal
282
+ */
283
+ function verifyJWT(token, rawDecodedJWT, keysCache, time = Date.now()) {
284
+ if (!rawDecodedJWT.header) {
285
+ throw new https_1.HttpsError('internal', 'Unable to verify JWT payload, the decoded JWT does not have a header property.');
286
+ }
287
+ const header = rawDecodedJWT.header;
288
+ let publicKey;
289
+ try {
290
+ if (invalidPublicKeys(keysCache, time)) {
291
+ refreshPublicKeys(keysCache);
292
+ }
293
+ publicKey = getPublicKeyFromHeader(header, keysCache.publicKeys);
294
+ return jwt.verify(token, publicKey, {
295
+ algorithms: [exports.JWT_ALG],
296
+ });
297
+ }
298
+ catch (err) {
299
+ __1.logger.error('Verifying the JWT failed', err);
300
+ }
301
+ // force refresh keys and retry one more time
302
+ try {
303
+ refreshPublicKeys(keysCache);
304
+ publicKey = getPublicKeyFromHeader(header, keysCache.publicKeys);
305
+ return jwt.verify(token, publicKey, {
306
+ algorithms: [exports.JWT_ALG],
307
+ });
308
+ }
309
+ catch (err) {
310
+ __1.logger.error('Verifying the JWT failed again', err);
311
+ throw new https_1.HttpsError('internal', 'Failed to verify the JWT.');
312
+ }
313
+ }
314
+ exports.verifyJWT = verifyJWT;
315
+ /**
316
+ * Helper function to parse the decoded metadata object into a UserMetaData object
317
+ * @internal
318
+ */
319
+ function parseMetadata(metadata) {
320
+ const creationTime = (metadata === null || metadata === void 0 ? void 0 : metadata.creation_time)
321
+ ? new Date(metadata.creation_time * 1000).toUTCString()
322
+ : null;
323
+ const lastSignInTime = (metadata === null || metadata === void 0 ? void 0 : metadata.last_sign_in_time)
324
+ ? new Date(metadata.last_sign_in_time * 1000).toUTCString()
325
+ : null;
326
+ return {
327
+ creationTime,
328
+ lastSignInTime,
329
+ };
330
+ }
331
+ exports.parseMetadata = parseMetadata;
332
+ /**
333
+ * Helper function to parse the decoded user info array into an AuthUserInfo array
334
+ * @internal
335
+ */
336
+ function parseProviderData(providerData) {
337
+ const providers = [];
338
+ for (const provider of providerData) {
339
+ providers.push({
340
+ uid: provider.uid,
341
+ displayName: provider.display_name,
342
+ email: provider.email,
343
+ photoURL: provider.photo_url,
344
+ providerId: provider.provider_id,
345
+ phoneNumber: provider.phone_number,
346
+ });
347
+ }
348
+ return providers;
349
+ }
350
+ exports.parseProviderData = parseProviderData;
351
+ /**
352
+ * Helper function to parse the date into a UTC string
353
+ * @internal
354
+ */
355
+ function parseDate(tokensValidAfterTime) {
356
+ if (!tokensValidAfterTime) {
357
+ return null;
358
+ }
359
+ tokensValidAfterTime = tokensValidAfterTime * 1000;
360
+ try {
361
+ const date = new Date(tokensValidAfterTime);
362
+ if (!isNaN(date.getTime())) {
363
+ return date.toUTCString();
364
+ }
365
+ }
366
+ catch (_a) { }
367
+ return null;
368
+ }
369
+ exports.parseDate = parseDate;
370
+ /**
371
+ * Helper function to parse the decoded enrolled factors into a valid MultiFactorSettings
372
+ * @internal
373
+ */
374
+ function parseMultiFactor(multiFactor) {
375
+ if (!multiFactor) {
376
+ return null;
377
+ }
378
+ const parsedEnrolledFactors = [];
379
+ for (const factor of multiFactor.enrolled_factors || []) {
380
+ if (!factor.uid) {
381
+ throw new https_1.HttpsError('internal', 'INTERNAL ASSERT FAILED: Invalid multi-factor info response');
382
+ }
383
+ const enrollmentTime = factor.enrollment_time
384
+ ? new Date(factor.enrollment_time).toUTCString()
385
+ : null;
386
+ parsedEnrolledFactors.push({
387
+ uid: factor.uid,
388
+ factorId: factor.phone_number
389
+ ? factor.factor_id || 'phone'
390
+ : factor.factor_id,
391
+ displayName: factor.display_name,
392
+ enrollmentTime,
393
+ phoneNumber: factor.phone_number,
394
+ });
395
+ }
396
+ if (parsedEnrolledFactors.length > 0) {
397
+ return {
398
+ enrolledFactors: parsedEnrolledFactors,
399
+ };
400
+ }
401
+ return null;
402
+ }
403
+ exports.parseMultiFactor = parseMultiFactor;
404
+ /**
405
+ * Parses the decoded user record into a valid UserRecord for use in the handler
406
+ * @internal
407
+ */
408
+ function parseAuthUserRecord(decodedJWTUserRecord) {
409
+ if (!decodedJWTUserRecord.uid) {
410
+ throw new https_1.HttpsError('internal', 'INTERNAL ASSERT FAILED: Invalid user response');
411
+ }
412
+ const disabled = decodedJWTUserRecord.disabled || false;
413
+ const metadata = parseMetadata(decodedJWTUserRecord.metadata);
414
+ const providerData = parseProviderData(decodedJWTUserRecord.provider_data);
415
+ const tokensValidAfterTime = parseDate(decodedJWTUserRecord.tokens_valid_after_time);
416
+ const multiFactor = parseMultiFactor(decodedJWTUserRecord.multi_factor);
417
+ return {
418
+ uid: decodedJWTUserRecord.uid,
419
+ email: decodedJWTUserRecord.email,
420
+ emailVerified: decodedJWTUserRecord.email_verified,
421
+ displayName: decodedJWTUserRecord.display_name,
422
+ photoURL: decodedJWTUserRecord.photo_url,
423
+ phoneNumber: decodedJWTUserRecord.phone_number,
424
+ disabled,
425
+ metadata,
426
+ providerData,
427
+ passwordHash: decodedJWTUserRecord.password_hash,
428
+ passwordSalt: decodedJWTUserRecord.password_salt,
429
+ customClaims: decodedJWTUserRecord.custom_claims,
430
+ tenantId: decodedJWTUserRecord.tenant_id,
431
+ tokensValidAfterTime,
432
+ multiFactor,
433
+ };
434
+ }
435
+ exports.parseAuthUserRecord = parseAuthUserRecord;
436
+ /** Helper to get the AdditionalUserInfo from the decoded jwt */
437
+ function parseAdditionalUserInfo(decodedJWT) {
438
+ let profile, username;
439
+ if (decodedJWT.raw_user_info)
440
+ try {
441
+ profile = JSON.parse(decodedJWT.raw_user_info);
442
+ }
443
+ catch (err) {
444
+ __1.logger.debug(`Parse Error: ${err.message}`);
445
+ }
446
+ if (profile) {
447
+ if (decodedJWT.sign_in_method === 'github.com') {
448
+ username = profile.login;
449
+ }
450
+ if (decodedJWT.sign_in_method === 'twitter.com') {
451
+ username = profile.screen_name;
452
+ }
453
+ }
454
+ return {
455
+ providerId: decodedJWT.sign_in_method === 'emailLink'
456
+ ? 'password'
457
+ : decodedJWT.sign_in_method,
458
+ profile,
459
+ username,
460
+ isNewUser: decodedJWT.event_type === 'beforeCreate' ? true : false,
461
+ };
462
+ }
463
+ /** Helper to get the Credential from the decoded jwt */
464
+ function parseAuthCredential(decodedJWT, time) {
465
+ if (!decodedJWT.sign_in_attributes &&
466
+ !decodedJWT.oauth_id_token &&
467
+ !decodedJWT.oauth_access_token &&
468
+ !decodedJWT.oauth_refresh_token) {
469
+ return null;
470
+ }
471
+ return {
472
+ claims: decodedJWT.sign_in_attributes,
473
+ idToken: decodedJWT.oauth_id_token,
474
+ accessToken: decodedJWT.oauth_access_token,
475
+ refreshToken: decodedJWT.oauth_refresh_token,
476
+ expirationTime: decodedJWT.oauth_expires_in
477
+ ? new Date(time + decodedJWT.oauth_expires_in * 1000).toUTCString()
478
+ : undefined,
479
+ secret: decodedJWT.oauth_token_secret,
480
+ providerId: decodedJWT.sign_in_method === 'emailLink'
481
+ ? 'password'
482
+ : decodedJWT.sign_in_method,
483
+ signInMethod: decodedJWT.sign_in_method,
484
+ };
485
+ }
486
+ /**
487
+ * Parses the decoded jwt into a valid AuthEventContext for use in the handler
488
+ * @internal
489
+ */
490
+ function parseAuthEventContext(decodedJWT, projectId, time = new Date().getTime()) {
491
+ const eventType = (EVENT_MAPPING[decodedJWT.event_type] || decodedJWT.event_type) +
492
+ (decodedJWT.sign_in_method ? `:${decodedJWT.sign_in_method}` : '');
493
+ return {
494
+ locale: decodedJWT.locale,
495
+ ipAddress: decodedJWT.ip_address,
496
+ userAgent: decodedJWT.user_agent,
497
+ eventId: decodedJWT.event_id,
498
+ eventType,
499
+ authType: !!decodedJWT.user_record ? 'USER' : 'UNAUTHENTICATED',
500
+ resource: {
501
+ // TODO(colerogers): figure out the correct service
502
+ service: 'identitytoolkit.googleapis.com',
503
+ name: !!decodedJWT.tenant_id
504
+ ? `projects/${projectId}/tenants/${decodedJWT.tenant_id}`
505
+ : `projects/${projectId}`,
506
+ },
507
+ timestamp: new Date(decodedJWT.iat * 1000).toUTCString(),
508
+ additionalUserInfo: parseAdditionalUserInfo(decodedJWT),
509
+ credential: parseAuthCredential(decodedJWT, time),
510
+ params: {},
511
+ };
512
+ }
513
+ exports.parseAuthEventContext = parseAuthEventContext;
514
+ /**
515
+ * Checks the handler response for invalid customClaims & sessionClaims objects
516
+ * @internal
517
+ */
518
+ function validateAuthResponse(eventType, authRequest) {
519
+ if (!authRequest) {
520
+ authRequest = {};
521
+ }
522
+ if (authRequest.customClaims) {
523
+ const invalidClaims = DISALLOWED_CUSTOM_CLAIMS.filter((claim) => authRequest.customClaims.hasOwnProperty(claim));
524
+ if (invalidClaims.length > 0) {
525
+ throw new https_1.HttpsError('invalid-argument', `The customClaims claims "${invalidClaims.join(',')}" are reserved and cannot be specified.`);
526
+ }
527
+ if (JSON.stringify(authRequest.customClaims).length > CLAIMS_MAX_PAYLOAD_SIZE) {
528
+ throw new https_1.HttpsError('invalid-argument', `The customClaims payload should not exceed ${CLAIMS_MAX_PAYLOAD_SIZE} characters.`);
529
+ }
530
+ }
531
+ if (eventType === 'beforeSignIn' &&
532
+ authRequest.sessionClaims) {
533
+ const invalidClaims = DISALLOWED_CUSTOM_CLAIMS.filter((claim) => authRequest.sessionClaims.hasOwnProperty(claim));
534
+ if (invalidClaims.length > 0) {
535
+ throw new https_1.HttpsError('invalid-argument', `The sessionClaims claims "${invalidClaims.join(',')}" are reserved and cannot be specified.`);
536
+ }
537
+ if (JSON.stringify(authRequest.sessionClaims)
538
+ .length > CLAIMS_MAX_PAYLOAD_SIZE) {
539
+ throw new https_1.HttpsError('invalid-argument', `The sessionClaims payload should not exceed ${CLAIMS_MAX_PAYLOAD_SIZE} characters.`);
540
+ }
541
+ const combinedClaims = {
542
+ ...authRequest.customClaims,
543
+ ...authRequest.sessionClaims,
544
+ };
545
+ if (JSON.stringify(combinedClaims).length > CLAIMS_MAX_PAYLOAD_SIZE) {
546
+ throw new https_1.HttpsError('invalid-argument', `The customClaims and sessionClaims payloads should not exceed ${CLAIMS_MAX_PAYLOAD_SIZE} characters combined.`);
547
+ }
548
+ }
549
+ }
550
+ exports.validateAuthResponse = validateAuthResponse;
551
+ /**
552
+ * Helper function to generate the update mask for the identity platform changed values
553
+ * @internal
554
+ */
555
+ function getUpdateMask(authResponse) {
556
+ if (!authResponse) {
557
+ return '';
558
+ }
559
+ const updateMask = [];
560
+ for (const key in authResponse) {
561
+ if (key === 'customClaims' || key === 'sessionClaims') {
562
+ continue;
563
+ }
564
+ if (authResponse.hasOwnProperty(key) &&
565
+ typeof authResponse[key] !== 'undefined') {
566
+ updateMask.push(key);
567
+ }
568
+ }
569
+ return updateMask.join(',');
570
+ }
571
+ exports.getUpdateMask = getUpdateMask;
572
+ /** @internal */
573
+ function createHandler(handler, eventType, keysCache) {
574
+ const wrappedHandler = wrapHandler(handler, eventType, keysCache);
575
+ return (req, res) => {
576
+ return new Promise((resolve) => {
577
+ res.on('finish', resolve);
578
+ resolve(wrappedHandler(req, res));
579
+ });
580
+ };
581
+ }
582
+ exports.createHandler = createHandler;
583
+ function wrapHandler(handler, eventType, keysCache) {
584
+ return async (req, res) => {
585
+ try {
586
+ const projectId = process.env.GCLOUD_PROJECT;
587
+ if (!isValidRequest(req)) {
588
+ __1.logger.error('Invalid request, unable to process');
589
+ throw new https_1.HttpsError('invalid-argument', 'Bad Request');
590
+ }
591
+ const rawDecodedJWT = decodeJWT(req.body.data.jwt);
592
+ const decodedPayload = shouldVerifyJWT()
593
+ ? verifyJWT(req.body.data.jwt, rawDecodedJWT, keysCache)
594
+ : rawDecodedJWT.payload;
595
+ checkDecodedToken(decodedPayload, eventType, projectId);
596
+ const authUserRecord = parseAuthUserRecord(decodedPayload.user_record);
597
+ const authEventContext = parseAuthEventContext(decodedPayload, projectId);
598
+ const authResponse = (await handler(authUserRecord, authEventContext)) || undefined;
599
+ validateAuthResponse(eventType, authResponse);
600
+ const updateMask = getUpdateMask(authResponse);
601
+ const result = {
602
+ userRecord: {
603
+ ...authResponse,
604
+ updateMask,
605
+ },
606
+ };
607
+ res.status(200);
608
+ res.setHeader('Content-Type', 'application/json');
609
+ res.send(JSON.stringify(result));
610
+ }
611
+ catch (err) {
612
+ if (!(err instanceof https_1.HttpsError)) {
613
+ // This doesn't count as an 'explicit' error.
614
+ __1.logger.error('Unhandled error', err);
615
+ err = new https_1.HttpsError('internal', 'An unexpected error occurred.');
616
+ }
617
+ res.status(err.code);
618
+ res.setHeader('Content-Type', 'application/json');
619
+ res.send({ error: err.toJson() });
620
+ }
621
+ };
622
+ }
@@ -0,0 +1,58 @@
1
+ import * as firebase from 'firebase-admin';
2
+ /** How a task should be retried in the event of a non-2xx return. */
3
+ export interface RetryConfig {
4
+ /**
5
+ * Maximum number of times a request should be attempted.
6
+ * If left unspecified, will default to 3.
7
+ */
8
+ maxAttempts?: number;
9
+ /**
10
+ * Maximum amount of time for retrying failed task.
11
+ * If left unspecified will retry indefinitely.
12
+ */
13
+ maxRetrySeconds?: number;
14
+ /**
15
+ * The maximum amount of time to wait between attempts.
16
+ * If left unspecified will default to 1hr.
17
+ */
18
+ maxBackoffSeconds?: number;
19
+ /**
20
+ * The maximum number of times to double the backoff between
21
+ * retries. If left unspecified will default to 16.
22
+ */
23
+ maxDoublings?: number;
24
+ /**
25
+ * The minimum time to wait between attempts. If left unspecified
26
+ * will default to 100ms.
27
+ */
28
+ minBackoffSeconds?: number;
29
+ }
30
+ /** How congestion control should be applied to the function. */
31
+ export interface RateLimits {
32
+ maxConcurrentDispatches?: number;
33
+ maxDispatchesPerSecond?: number;
34
+ }
35
+ export interface AuthData {
36
+ uid: string;
37
+ token: firebase.auth.DecodedIdToken;
38
+ }
39
+ /** Metadata about a call to a Task Queue function. */
40
+ export interface TaskContext {
41
+ /**
42
+ * The result of decoding and verifying an ODIC token.
43
+ */
44
+ auth?: AuthData;
45
+ }
46
+ /**
47
+ * The request used to call a Task Queue function.
48
+ */
49
+ export interface Request<T = any> {
50
+ /**
51
+ * The parameters used by a client when calling this function.
52
+ */
53
+ data: T;
54
+ /**
55
+ * The result of decoding and verifying an ODIC token.
56
+ */
57
+ auth?: AuthData;
58
+ }