1 | "use strict";
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | Object.defineProperty(exports, "__esModule", { value: true });
|
24 | exports.wrapHandler = exports.getUpdateMask = exports.validateAuthResponse = exports.parseAuthEventContext = exports.generateResponsePayload = exports.parseAuthUserRecord = exports.parseMultiFactor = exports.parseDate = exports.parseProviderData = exports.parseMetadata = exports.isValidRequest = exports.userRecordConstructor = exports.UserRecordMetadata = exports.HttpsError = void 0;
|
25 | const auth = require("firebase-admin/auth");
|
26 | const logger = require("../../logger");
|
27 | const app_1 = require("../app");
|
28 | const debug_1 = require("../debug");
|
29 | const https_1 = require("./https");
|
30 | Object.defineProperty(exports, "HttpsError", { enumerable: true, get: function () { return https_1.HttpsError; } });
|
31 | const DISALLOWED_CUSTOM_CLAIMS = [
|
32 | "acr",
|
33 | "amr",
|
34 | "at_hash",
|
35 | "aud",
|
36 | "auth_time",
|
37 | "azp",
|
38 | "cnf",
|
39 | "c_hash",
|
40 | "exp",
|
41 | "iat",
|
42 | "iss",
|
43 | "jti",
|
44 | "nbf",
|
45 | "nonce",
|
46 | "firebase",
|
47 | ];
|
48 | const CLAIMS_MAX_PAYLOAD_SIZE = 1000;
|
49 | const EVENT_MAPPING = {
|
50 | beforeCreate: "providers/cloud.auth/eventTypes/user.beforeCreate",
|
51 | beforeSignIn: "providers/cloud.auth/eventTypes/user.beforeSignIn",
|
52 | beforeSendEmail: "providers/cloud.auth/eventTypes/user.beforeSendEmail",
|
53 | beforeSendSms: "providers/cloud.auth/eventTypes/user.beforeSendSms",
|
54 | };
|
55 |
|
56 |
|
57 |
|
58 | class UserRecordMetadata {
|
59 | constructor(creationTime, lastSignInTime) {
|
60 | this.creationTime = creationTime;
|
61 | this.lastSignInTime = lastSignInTime;
|
62 | }
|
63 |
|
64 | toJSON() {
|
65 | return {
|
66 | creationTime: this.creationTime,
|
67 | lastSignInTime: this.lastSignInTime,
|
68 | };
|
69 | }
|
70 | }
|
71 | exports.UserRecordMetadata = UserRecordMetadata;
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 | function userRecordConstructor(wireData) {
|
78 |
|
79 | const falseyValues = {
|
80 | email: null,
|
81 | emailVerified: false,
|
82 | displayName: null,
|
83 | photoURL: null,
|
84 | phoneNumber: null,
|
85 | disabled: false,
|
86 | providerData: [],
|
87 | customClaims: {},
|
88 | passwordSalt: null,
|
89 | passwordHash: null,
|
90 | tokensValidAfterTime: null,
|
91 | };
|
92 | const record = { ...falseyValues, ...wireData };
|
93 | const meta = record.metadata;
|
94 | if (meta) {
|
95 | record.metadata = new UserRecordMetadata(meta.createdAt || meta.creationTime, meta.lastSignedInAt || meta.lastSignInTime);
|
96 | }
|
97 | else {
|
98 | record.metadata = new UserRecordMetadata(null, null);
|
99 | }
|
100 | record.toJSON = () => {
|
101 | const { uid, email, emailVerified, displayName, photoURL, phoneNumber, disabled, passwordHash, passwordSalt, tokensValidAfterTime, } = record;
|
102 | const json = {
|
103 | uid,
|
104 | email,
|
105 | emailVerified,
|
106 | displayName,
|
107 | photoURL,
|
108 | phoneNumber,
|
109 | disabled,
|
110 | passwordHash,
|
111 | passwordSalt,
|
112 | tokensValidAfterTime,
|
113 | };
|
114 | json.metadata = record.metadata.toJSON();
|
115 | json.customClaims = JSON.parse(JSON.stringify(record.customClaims));
|
116 | json.providerData = record.providerData.map((entry) => {
|
117 | const newEntry = { ...entry };
|
118 | newEntry.toJSON = () => entry;
|
119 | return newEntry;
|
120 | });
|
121 | return json;
|
122 | };
|
123 | return record;
|
124 | }
|
125 | exports.userRecordConstructor = userRecordConstructor;
|
126 |
|
127 |
|
128 |
|
129 |
|
130 | function isValidRequest(req) {
|
131 | var _a, _b;
|
132 | if (req.method !== "POST") {
|
133 | logger.warn(`Request has invalid method "${req.method}".`);
|
134 | return false;
|
135 | }
|
136 | const contentType = (req.header("Content-Type") || "").toLowerCase();
|
137 | if (!contentType.includes("application/json")) {
|
138 | logger.warn("Request has invalid header Content-Type.");
|
139 | return false;
|
140 | }
|
141 | if (!((_b = (_a = req.body) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.jwt)) {
|
142 | logger.warn("Request has an invalid body.");
|
143 | return false;
|
144 | }
|
145 | return true;
|
146 | }
|
147 | exports.isValidRequest = isValidRequest;
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 | function unsafeDecodeAuthBlockingToken(token) {
|
156 | const decoded = (0, https_1.unsafeDecodeToken)(token);
|
157 | decoded.uid = decoded.sub;
|
158 | return decoded;
|
159 | }
|
160 |
|
161 |
|
162 |
|
163 |
|
164 | function parseMetadata(metadata) {
|
165 | const creationTime = (metadata === null || metadata === void 0 ? void 0 : metadata.creation_time)
|
166 | ? new Date(metadata.creation_time).toUTCString()
|
167 | : null;
|
168 | const lastSignInTime = (metadata === null || metadata === void 0 ? void 0 : metadata.last_sign_in_time)
|
169 | ? new Date(metadata.last_sign_in_time).toUTCString()
|
170 | : null;
|
171 | return {
|
172 | creationTime,
|
173 | lastSignInTime,
|
174 | };
|
175 | }
|
176 | exports.parseMetadata = parseMetadata;
|
177 |
|
178 |
|
179 |
|
180 |
|
181 | function parseProviderData(providerData) {
|
182 | const providers = [];
|
183 | for (const provider of providerData) {
|
184 | providers.push({
|
185 | uid: provider.uid,
|
186 | displayName: provider.display_name,
|
187 | email: provider.email,
|
188 | photoURL: provider.photo_url,
|
189 | providerId: provider.provider_id,
|
190 | phoneNumber: provider.phone_number,
|
191 | });
|
192 | }
|
193 | return providers;
|
194 | }
|
195 | exports.parseProviderData = parseProviderData;
|
196 |
|
197 |
|
198 |
|
199 |
|
200 | function parseDate(tokensValidAfterTime) {
|
201 | if (!tokensValidAfterTime) {
|
202 | return null;
|
203 | }
|
204 | tokensValidAfterTime = tokensValidAfterTime * 1000;
|
205 | try {
|
206 | const date = new Date(tokensValidAfterTime);
|
207 | if (!isNaN(date.getTime())) {
|
208 | return date.toUTCString();
|
209 | }
|
210 | }
|
211 | catch {
|
212 |
|
213 | }
|
214 | return null;
|
215 | }
|
216 | exports.parseDate = parseDate;
|
217 |
|
218 |
|
219 |
|
220 |
|
221 | function parseMultiFactor(multiFactor) {
|
222 | if (!multiFactor) {
|
223 | return null;
|
224 | }
|
225 | const parsedEnrolledFactors = [];
|
226 | for (const factor of multiFactor.enrolled_factors || []) {
|
227 | if (!factor.uid) {
|
228 | throw new https_1.HttpsError("internal", "INTERNAL ASSERT FAILED: Invalid multi-factor info response");
|
229 | }
|
230 | const enrollmentTime = factor.enrollment_time
|
231 | ? new Date(factor.enrollment_time).toUTCString()
|
232 | : null;
|
233 | parsedEnrolledFactors.push({
|
234 | uid: factor.uid,
|
235 | factorId: factor.phone_number ? factor.factor_id || "phone" : factor.factor_id,
|
236 | displayName: factor.display_name,
|
237 | enrollmentTime,
|
238 | phoneNumber: factor.phone_number,
|
239 | });
|
240 | }
|
241 | if (parsedEnrolledFactors.length > 0) {
|
242 | return {
|
243 | enrolledFactors: parsedEnrolledFactors,
|
244 | };
|
245 | }
|
246 | return null;
|
247 | }
|
248 | exports.parseMultiFactor = parseMultiFactor;
|
249 |
|
250 |
|
251 |
|
252 |
|
253 | function parseAuthUserRecord(decodedJWTUserRecord) {
|
254 | if (!decodedJWTUserRecord.uid) {
|
255 | throw new https_1.HttpsError("internal", "INTERNAL ASSERT FAILED: Invalid user response");
|
256 | }
|
257 | const disabled = decodedJWTUserRecord.disabled || false;
|
258 | const metadata = parseMetadata(decodedJWTUserRecord.metadata);
|
259 | const providerData = parseProviderData(decodedJWTUserRecord.provider_data);
|
260 | const tokensValidAfterTime = parseDate(decodedJWTUserRecord.tokens_valid_after_time);
|
261 | const multiFactor = parseMultiFactor(decodedJWTUserRecord.multi_factor);
|
262 | return {
|
263 | uid: decodedJWTUserRecord.uid,
|
264 | email: decodedJWTUserRecord.email,
|
265 | emailVerified: decodedJWTUserRecord.email_verified,
|
266 | displayName: decodedJWTUserRecord.display_name,
|
267 | photoURL: decodedJWTUserRecord.photo_url,
|
268 | phoneNumber: decodedJWTUserRecord.phone_number,
|
269 | disabled,
|
270 | metadata,
|
271 | providerData,
|
272 | passwordHash: decodedJWTUserRecord.password_hash,
|
273 | passwordSalt: decodedJWTUserRecord.password_salt,
|
274 | customClaims: decodedJWTUserRecord.custom_claims,
|
275 | tenantId: decodedJWTUserRecord.tenant_id,
|
276 | tokensValidAfterTime,
|
277 | multiFactor,
|
278 | };
|
279 | }
|
280 | exports.parseAuthUserRecord = parseAuthUserRecord;
|
281 |
|
282 | function parseAdditionalUserInfo(decodedJWT) {
|
283 | let profile;
|
284 | let username;
|
285 | if (decodedJWT.raw_user_info) {
|
286 | try {
|
287 | profile = JSON.parse(decodedJWT.raw_user_info);
|
288 | }
|
289 | catch (err) {
|
290 | logger.debug(`Parse Error: ${err.message}`);
|
291 | }
|
292 | }
|
293 | if (profile) {
|
294 | if (decodedJWT.sign_in_method === "github.com") {
|
295 | username = profile.login;
|
296 | }
|
297 | if (decodedJWT.sign_in_method === "twitter.com") {
|
298 | username = profile.screen_name;
|
299 | }
|
300 | }
|
301 | return {
|
302 | providerId: decodedJWT.sign_in_method === "emailLink" ? "password" : decodedJWT.sign_in_method,
|
303 | profile,
|
304 | username,
|
305 | isNewUser: decodedJWT.event_type === "beforeCreate" ? true : false,
|
306 | recaptchaScore: decodedJWT.recaptcha_score,
|
307 | email: decodedJWT.email,
|
308 | phoneNumber: decodedJWT.phone_number,
|
309 | };
|
310 | }
|
311 |
|
312 |
|
313 |
|
314 |
|
315 | function generateResponsePayload(authResponse) {
|
316 | if (!authResponse) {
|
317 | return {};
|
318 | }
|
319 | const { recaptchaActionOverride, ...formattedAuthResponse } = authResponse;
|
320 | const result = {};
|
321 | const updateMask = getUpdateMask(formattedAuthResponse);
|
322 | if (updateMask.length !== 0) {
|
323 | result.userRecord = {
|
324 | ...formattedAuthResponse,
|
325 | updateMask,
|
326 | };
|
327 | }
|
328 | if (recaptchaActionOverride !== undefined) {
|
329 | result.recaptchaActionOverride = recaptchaActionOverride;
|
330 | }
|
331 | return result;
|
332 | }
|
333 | exports.generateResponsePayload = generateResponsePayload;
|
334 |
|
335 | function parseAuthCredential(decodedJWT, time) {
|
336 | if (!decodedJWT.sign_in_attributes &&
|
337 | !decodedJWT.oauth_id_token &&
|
338 | !decodedJWT.oauth_access_token &&
|
339 | !decodedJWT.oauth_refresh_token) {
|
340 | return null;
|
341 | }
|
342 | return {
|
343 | claims: decodedJWT.sign_in_attributes,
|
344 | idToken: decodedJWT.oauth_id_token,
|
345 | accessToken: decodedJWT.oauth_access_token,
|
346 | refreshToken: decodedJWT.oauth_refresh_token,
|
347 | expirationTime: decodedJWT.oauth_expires_in
|
348 | ? new Date(time + decodedJWT.oauth_expires_in * 1000).toUTCString()
|
349 | : undefined,
|
350 | secret: decodedJWT.oauth_token_secret,
|
351 | providerId: decodedJWT.sign_in_method === "emailLink" ? "password" : decodedJWT.sign_in_method,
|
352 | signInMethod: decodedJWT.sign_in_method,
|
353 | };
|
354 | }
|
355 |
|
356 |
|
357 |
|
358 |
|
359 | function parseAuthEventContext(decodedJWT, projectId, time = new Date().getTime()) {
|
360 | const eventType = (EVENT_MAPPING[decodedJWT.event_type] || decodedJWT.event_type) +
|
361 | (decodedJWT.sign_in_method ? `:${decodedJWT.sign_in_method}` : "");
|
362 | return {
|
363 | locale: decodedJWT.locale,
|
364 | ipAddress: decodedJWT.ip_address,
|
365 | userAgent: decodedJWT.user_agent,
|
366 | eventId: decodedJWT.event_id,
|
367 | eventType,
|
368 | authType: decodedJWT.user_record ? "USER" : "UNAUTHENTICATED",
|
369 | resource: {
|
370 |
|
371 | service: "identitytoolkit.googleapis.com",
|
372 | name: decodedJWT.tenant_id
|
373 | ? `projects/${projectId}/tenants/${decodedJWT.tenant_id}`
|
374 | : `projects/${projectId}`,
|
375 | },
|
376 | timestamp: new Date(decodedJWT.iat * 1000).toUTCString(),
|
377 | additionalUserInfo: parseAdditionalUserInfo(decodedJWT),
|
378 | credential: parseAuthCredential(decodedJWT, time),
|
379 | emailType: decodedJWT.email_type,
|
380 | smsType: decodedJWT.sms_type,
|
381 | params: {},
|
382 | };
|
383 | }
|
384 | exports.parseAuthEventContext = parseAuthEventContext;
|
385 |
|
386 |
|
387 |
|
388 |
|
389 | function validateAuthResponse(eventType, authRequest) {
|
390 | if (!authRequest) {
|
391 | authRequest = {};
|
392 | }
|
393 | if (authRequest.customClaims) {
|
394 | const invalidClaims = DISALLOWED_CUSTOM_CLAIMS.filter((claim) => authRequest.customClaims.hasOwnProperty(claim));
|
395 | if (invalidClaims.length > 0) {
|
396 | throw new https_1.HttpsError("invalid-argument", `The customClaims claims "${invalidClaims.join(",")}" are reserved and cannot be specified.`);
|
397 | }
|
398 | if (JSON.stringify(authRequest.customClaims).length > CLAIMS_MAX_PAYLOAD_SIZE) {
|
399 | throw new https_1.HttpsError("invalid-argument", `The customClaims payload should not exceed ${CLAIMS_MAX_PAYLOAD_SIZE} characters.`);
|
400 | }
|
401 | }
|
402 | if (eventType === "beforeSignIn" && authRequest.sessionClaims) {
|
403 | const invalidClaims = DISALLOWED_CUSTOM_CLAIMS.filter((claim) => authRequest.sessionClaims.hasOwnProperty(claim));
|
404 | if (invalidClaims.length > 0) {
|
405 | throw new https_1.HttpsError("invalid-argument", `The sessionClaims claims "${invalidClaims.join(",")}" are reserved and cannot be specified.`);
|
406 | }
|
407 | if (JSON.stringify(authRequest.sessionClaims).length >
|
408 | CLAIMS_MAX_PAYLOAD_SIZE) {
|
409 | throw new https_1.HttpsError("invalid-argument", `The sessionClaims payload should not exceed ${CLAIMS_MAX_PAYLOAD_SIZE} characters.`);
|
410 | }
|
411 | const combinedClaims = {
|
412 | ...authRequest.customClaims,
|
413 | ...authRequest.sessionClaims,
|
414 | };
|
415 | if (JSON.stringify(combinedClaims).length > CLAIMS_MAX_PAYLOAD_SIZE) {
|
416 | throw new https_1.HttpsError("invalid-argument", `The customClaims and sessionClaims payloads should not exceed ${CLAIMS_MAX_PAYLOAD_SIZE} characters combined.`);
|
417 | }
|
418 | }
|
419 | }
|
420 | exports.validateAuthResponse = validateAuthResponse;
|
421 |
|
422 |
|
423 |
|
424 |
|
425 | function getUpdateMask(authResponse) {
|
426 | if (!authResponse) {
|
427 | return "";
|
428 | }
|
429 | const updateMask = [];
|
430 | for (const key in authResponse) {
|
431 | if (authResponse.hasOwnProperty(key) && typeof authResponse[key] !== "undefined") {
|
432 | updateMask.push(key);
|
433 | }
|
434 | }
|
435 | return updateMask.join(",");
|
436 | }
|
437 | exports.getUpdateMask = getUpdateMask;
|
438 |
|
439 | function wrapHandler(eventType, handler) {
|
440 | return async (req, res) => {
|
441 | try {
|
442 | const projectId = process.env.GCLOUD_PROJECT;
|
443 | if (!isValidRequest(req)) {
|
444 | logger.error("Invalid request, unable to process");
|
445 | throw new https_1.HttpsError("invalid-argument", "Bad Request");
|
446 | }
|
447 | if (!auth.getAuth((0, app_1.getApp)())._verifyAuthBlockingToken) {
|
448 | throw new Error("Cannot validate Auth Blocking token. Please update Firebase Admin SDK to >= v10.1.0");
|
449 | }
|
450 | const decodedPayload = (0, debug_1.isDebugFeatureEnabled)("skipTokenVerification")
|
451 | ? unsafeDecodeAuthBlockingToken(req.body.data.jwt)
|
452 | : handler.platform === "gcfv1"
|
453 | ? await auth.getAuth((0, app_1.getApp)())._verifyAuthBlockingToken(req.body.data.jwt)
|
454 | : await auth.getAuth((0, app_1.getApp)())._verifyAuthBlockingToken(req.body.data.jwt, "run.app");
|
455 | let authUserRecord;
|
456 | if (decodedPayload.event_type === "beforeCreate" ||
|
457 | decodedPayload.event_type === "beforeSignIn") {
|
458 | authUserRecord = parseAuthUserRecord(decodedPayload.user_record);
|
459 | }
|
460 | const authEventContext = parseAuthEventContext(decodedPayload, projectId);
|
461 | let authResponse;
|
462 | if (handler.platform === "gcfv1") {
|
463 | authResponse = authUserRecord
|
464 | ? (await handler(authUserRecord, authEventContext)) || undefined
|
465 | : (await handler(authEventContext)) || undefined;
|
466 | }
|
467 | else {
|
468 | authResponse =
|
469 | (await handler({
|
470 | ...authEventContext,
|
471 | data: authUserRecord,
|
472 | })) || undefined;
|
473 | }
|
474 | validateAuthResponse(eventType, authResponse);
|
475 | const result = generateResponsePayload(authResponse);
|
476 | res.status(200);
|
477 | res.setHeader("Content-Type", "application/json");
|
478 | res.send(JSON.stringify(result));
|
479 | }
|
480 | catch (err) {
|
481 | let httpErr = err;
|
482 | if (!(httpErr instanceof https_1.HttpsError)) {
|
483 |
|
484 | logger.error("Unhandled error", err);
|
485 | httpErr = new https_1.HttpsError("internal", "An unexpected error occurred.");
|
486 | }
|
487 | const { status } = httpErr.httpErrorCode;
|
488 | const body = { error: httpErr.toJSON() };
|
489 | res.setHeader("Content-Type", "application/json");
|
490 | res.status(status).send(body);
|
491 | }
|
492 | };
|
493 | }
|
494 | exports.wrapHandler = wrapHandler;
|