firebase-functions 3.18.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.
- package/lib/apps.js +1 -1
- package/lib/bin/firebase-functions.js +16 -14
- package/lib/cloud-functions.js +12 -12
- package/lib/common/providers/https.d.ts +20 -49
- package/lib/common/providers/https.js +10 -50
- package/lib/common/providers/identity.d.ts +187 -4
- package/lib/common/providers/identity.js +553 -27
- package/lib/common/providers/tasks.d.ts +58 -0
- package/lib/common/providers/tasks.js +68 -0
- package/lib/function-builder.d.ts +4 -1
- package/lib/function-builder.js +6 -1
- package/lib/handler-builder.d.ts +13 -2
- package/lib/handler-builder.js +18 -5
- package/lib/index.d.ts +2 -1
- package/lib/index.js +4 -2
- package/lib/logger/compat.js +1 -1
- package/lib/providers/analytics.js +1 -1
- package/lib/providers/auth.js +2 -2
- package/lib/providers/database.js +12 -12
- package/lib/providers/firestore.js +7 -7
- package/lib/providers/https.d.ts +2 -44
- package/lib/providers/https.js +8 -56
- package/lib/providers/pubsub.js +2 -2
- package/lib/providers/remoteConfig.js +1 -1
- package/lib/providers/storage.js +2 -2
- package/lib/providers/tasks.d.ts +46 -0
- package/lib/providers/tasks.js +75 -0
- package/lib/providers/testLab.js +1 -1
- package/lib/runtime/loader.js +1 -2
- package/lib/runtime/manifest.d.ts +5 -3
- package/lib/runtime/manifest.js +21 -0
- package/lib/setup.js +3 -3
- package/lib/v2/index.d.ts +2 -1
- package/lib/v2/index.js +3 -1
- package/lib/v2/options.d.ts +2 -2
- package/lib/v2/options.js +23 -14
- package/lib/v2/providers/alerts/alerts.js +2 -2
- package/lib/v2/providers/alerts/appDistribution.js +1 -1
- package/lib/v2/providers/alerts/billing.d.ts +2 -2
- package/lib/v2/providers/alerts/billing.js +6 -6
- package/lib/v2/providers/alerts/crashlytics.js +1 -1
- package/lib/v2/providers/https.d.ts +2 -20
- package/lib/v2/providers/https.js +4 -43
- package/lib/v2/providers/pubsub.js +1 -1
- package/lib/v2/providers/storage.js +3 -5
- package/lib/v2/providers/tasks.d.ts +22 -0
- package/lib/v2/providers/tasks.js +89 -0
- package/package.json +24 -14
|
@@ -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
|
|
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 =
|
|
64
|
-
const meta =
|
|
100
|
+
const record = { ...falseyValues, ...wireData };
|
|
101
|
+
const meta = record['metadata'];
|
|
65
102
|
if (meta) {
|
|
66
|
-
|
|
103
|
+
record['metadata'] = new UserRecordMetadata(meta.createdAt || meta.creationTime, meta.lastSignedInAt || meta.lastSignInTime);
|
|
67
104
|
}
|
|
68
105
|
else {
|
|
69
|
-
|
|
106
|
+
record['metadata'] = new UserRecordMetadata(null, null);
|
|
70
107
|
}
|
|
71
|
-
|
|
72
|
-
|
|
108
|
+
for (const entry of Object.entries(record.providerData)) {
|
|
109
|
+
entry['toJSON'] = () => {
|
|
73
110
|
return entry;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
json
|
|
91
|
-
json
|
|
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
|
+
}
|