UNPKG

18.3 kBJavaScriptView Raw
1/*! firebase-admin v12.0.0 */
2"use strict";
3/*!
4 * Copyright 2018 Google Inc.
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18Object.defineProperty(exports, "__esModule", { value: true });
19exports.UserImportBuilder = exports.convertMultiFactorInfoToServerFormat = void 0;
20const deep_copy_1 = require("../utils/deep-copy");
21const utils = require("../utils");
22const validator = require("../utils/validator");
23const error_1 = require("../utils/error");
24/**
25 * Converts a client format second factor object to server format.
26 * @param multiFactorInfo - The client format second factor.
27 * @returns The corresponding AuthFactorInfo server request format.
28 */
29function convertMultiFactorInfoToServerFormat(multiFactorInfo) {
30 let enrolledAt;
31 if (typeof multiFactorInfo.enrollmentTime !== 'undefined') {
32 if (validator.isUTCDateString(multiFactorInfo.enrollmentTime)) {
33 // Convert from UTC date string (client side format) to ISO date string (server side format).
34 enrolledAt = new Date(multiFactorInfo.enrollmentTime).toISOString();
35 }
36 else {
37 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ENROLLMENT_TIME, `The second factor "enrollmentTime" for "${multiFactorInfo.uid}" must be a valid ` +
38 'UTC date string.');
39 }
40 }
41 // Currently only phone second factors are supported.
42 if (isPhoneFactor(multiFactorInfo)) {
43 // If any required field is missing or invalid, validation will still fail later.
44 const authFactorInfo = {
45 mfaEnrollmentId: multiFactorInfo.uid,
46 displayName: multiFactorInfo.displayName,
47 // Required for all phone second factors.
48 phoneInfo: multiFactorInfo.phoneNumber,
49 enrolledAt,
50 };
51 for (const objKey in authFactorInfo) {
52 if (typeof authFactorInfo[objKey] === 'undefined') {
53 delete authFactorInfo[objKey];
54 }
55 }
56 return authFactorInfo;
57 }
58 else {
59 // Unsupported second factor.
60 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.UNSUPPORTED_SECOND_FACTOR, `Unsupported second factor "${JSON.stringify(multiFactorInfo)}" provided.`);
61 }
62}
63exports.convertMultiFactorInfoToServerFormat = convertMultiFactorInfoToServerFormat;
64function isPhoneFactor(multiFactorInfo) {
65 return multiFactorInfo.factorId === 'phone';
66}
67/**
68 * @param {any} obj The object to check for number field within.
69 * @param {string} key The entry key.
70 * @returns {number} The corresponding number if available. Otherwise, NaN.
71 */
72function getNumberField(obj, key) {
73 if (typeof obj[key] !== 'undefined' && obj[key] !== null) {
74 return parseInt(obj[key].toString(), 10);
75 }
76 return NaN;
77}
78/**
79 * Converts a UserImportRecord to a UploadAccountUser object. Throws an error when invalid
80 * fields are provided.
81 * @param {UserImportRecord} user The UserImportRecord to conver to UploadAccountUser.
82 * @param {ValidatorFunction=} userValidator The user validator function.
83 * @returns {UploadAccountUser} The corresponding UploadAccountUser to return.
84 */
85function populateUploadAccountUser(user, userValidator) {
86 const result = {
87 localId: user.uid,
88 email: user.email,
89 emailVerified: user.emailVerified,
90 displayName: user.displayName,
91 disabled: user.disabled,
92 photoUrl: user.photoURL,
93 phoneNumber: user.phoneNumber,
94 providerUserInfo: [],
95 mfaInfo: [],
96 tenantId: user.tenantId,
97 customAttributes: user.customClaims && JSON.stringify(user.customClaims),
98 };
99 if (typeof user.passwordHash !== 'undefined') {
100 if (!validator.isBuffer(user.passwordHash)) {
101 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PASSWORD_HASH);
102 }
103 result.passwordHash = utils.toWebSafeBase64(user.passwordHash);
104 }
105 if (typeof user.passwordSalt !== 'undefined') {
106 if (!validator.isBuffer(user.passwordSalt)) {
107 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PASSWORD_SALT);
108 }
109 result.salt = utils.toWebSafeBase64(user.passwordSalt);
110 }
111 if (validator.isNonNullObject(user.metadata)) {
112 if (validator.isNonEmptyString(user.metadata.creationTime)) {
113 result.createdAt = new Date(user.metadata.creationTime).getTime();
114 }
115 if (validator.isNonEmptyString(user.metadata.lastSignInTime)) {
116 result.lastLoginAt = new Date(user.metadata.lastSignInTime).getTime();
117 }
118 }
119 if (validator.isArray(user.providerData)) {
120 user.providerData.forEach((providerData) => {
121 result.providerUserInfo.push({
122 providerId: providerData.providerId,
123 rawId: providerData.uid,
124 email: providerData.email,
125 displayName: providerData.displayName,
126 photoUrl: providerData.photoURL,
127 });
128 });
129 }
130 // Convert user.multiFactor.enrolledFactors to server format.
131 if (validator.isNonNullObject(user.multiFactor) &&
132 validator.isNonEmptyArray(user.multiFactor.enrolledFactors)) {
133 user.multiFactor.enrolledFactors.forEach((multiFactorInfo) => {
134 result.mfaInfo.push(convertMultiFactorInfoToServerFormat(multiFactorInfo));
135 });
136 }
137 // Remove blank fields.
138 let key;
139 for (key in result) {
140 if (typeof result[key] === 'undefined') {
141 delete result[key];
142 }
143 }
144 if (result.providerUserInfo.length === 0) {
145 delete result.providerUserInfo;
146 }
147 if (result.mfaInfo.length === 0) {
148 delete result.mfaInfo;
149 }
150 // Validate the constructured user individual request. This will throw if an error
151 // is detected.
152 if (typeof userValidator === 'function') {
153 userValidator(result);
154 }
155 return result;
156}
157/**
158 * Class that provides a helper for building/validating uploadAccount requests and
159 * UserImportResult responses.
160 */
161class UserImportBuilder {
162 /**
163 * @param {UserImportRecord[]} users The list of user records to import.
164 * @param {UserImportOptions=} options The import options which includes hashing
165 * algorithm details.
166 * @param {ValidatorFunction=} userRequestValidator The user request validator function.
167 * @constructor
168 */
169 constructor(users, options, userRequestValidator) {
170 this.requiresHashOptions = false;
171 this.validatedUsers = [];
172 this.userImportResultErrors = [];
173 this.indexMap = {};
174 this.validatedUsers = this.populateUsers(users, userRequestValidator);
175 this.validatedOptions = this.populateOptions(options, this.requiresHashOptions);
176 }
177 /**
178 * Returns the corresponding constructed uploadAccount request.
179 * @returns {UploadAccountRequest} The constructed uploadAccount request.
180 */
181 buildRequest() {
182 const users = this.validatedUsers.map((user) => {
183 return (0, deep_copy_1.deepCopy)(user);
184 });
185 return (0, deep_copy_1.deepExtend)({ users }, (0, deep_copy_1.deepCopy)(this.validatedOptions));
186 }
187 /**
188 * Populates the UserImportResult using the client side detected errors and the server
189 * side returned errors.
190 * @returns {UserImportResult} The user import result based on the returned failed
191 * uploadAccount response.
192 */
193 buildResponse(failedUploads) {
194 // Initialize user import result.
195 const importResult = {
196 successCount: this.validatedUsers.length,
197 failureCount: this.userImportResultErrors.length,
198 errors: (0, deep_copy_1.deepCopy)(this.userImportResultErrors),
199 };
200 importResult.failureCount += failedUploads.length;
201 importResult.successCount -= failedUploads.length;
202 failedUploads.forEach((failedUpload) => {
203 importResult.errors.push({
204 // Map backend request index to original developer provided array index.
205 index: this.indexMap[failedUpload.index],
206 error: new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_USER_IMPORT, failedUpload.message),
207 });
208 });
209 // Sort errors by index.
210 importResult.errors.sort((a, b) => {
211 return a.index - b.index;
212 });
213 // Return sorted result.
214 return importResult;
215 }
216 /**
217 * Validates and returns the hashing options of the uploadAccount request.
218 * Throws an error whenever an invalid or missing options is detected.
219 * @param {UserImportOptions} options The UserImportOptions.
220 * @param {boolean} requiresHashOptions Whether to require hash options.
221 * @returns {UploadAccountOptions} The populated UploadAccount options.
222 */
223 populateOptions(options, requiresHashOptions) {
224 let populatedOptions;
225 if (!requiresHashOptions) {
226 return {};
227 }
228 if (!validator.isNonNullObject(options)) {
229 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, '"UserImportOptions" are required when importing users with passwords.');
230 }
231 if (!validator.isNonNullObject(options.hash)) {
232 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.MISSING_HASH_ALGORITHM, '"hash.algorithm" is missing from the provided "UserImportOptions".');
233 }
234 if (typeof options.hash.algorithm === 'undefined' ||
235 !validator.isNonEmptyString(options.hash.algorithm)) {
236 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_ALGORITHM, '"hash.algorithm" must be a string matching the list of supported algorithms.');
237 }
238 let rounds;
239 switch (options.hash.algorithm) {
240 case 'HMAC_SHA512':
241 case 'HMAC_SHA256':
242 case 'HMAC_SHA1':
243 case 'HMAC_MD5':
244 if (!validator.isBuffer(options.hash.key)) {
245 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_KEY, 'A non-empty "hash.key" byte buffer must be provided for ' +
246 `hash algorithm ${options.hash.algorithm}.`);
247 }
248 populatedOptions = {
249 hashAlgorithm: options.hash.algorithm,
250 signerKey: utils.toWebSafeBase64(options.hash.key),
251 };
252 break;
253 case 'MD5':
254 case 'SHA1':
255 case 'SHA256':
256 case 'SHA512': {
257 // MD5 is [0,8192] but SHA1, SHA256, and SHA512 are [1,8192]
258 rounds = getNumberField(options.hash, 'rounds');
259 const minRounds = options.hash.algorithm === 'MD5' ? 0 : 1;
260 if (isNaN(rounds) || rounds < minRounds || rounds > 8192) {
261 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_ROUNDS, `A valid "hash.rounds" number between ${minRounds} and 8192 must be provided for ` +
262 `hash algorithm ${options.hash.algorithm}.`);
263 }
264 populatedOptions = {
265 hashAlgorithm: options.hash.algorithm,
266 rounds,
267 };
268 break;
269 }
270 case 'PBKDF_SHA1':
271 case 'PBKDF2_SHA256':
272 rounds = getNumberField(options.hash, 'rounds');
273 if (isNaN(rounds) || rounds < 0 || rounds > 120000) {
274 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_ROUNDS, 'A valid "hash.rounds" number between 0 and 120000 must be provided for ' +
275 `hash algorithm ${options.hash.algorithm}.`);
276 }
277 populatedOptions = {
278 hashAlgorithm: options.hash.algorithm,
279 rounds,
280 };
281 break;
282 case 'SCRYPT': {
283 if (!validator.isBuffer(options.hash.key)) {
284 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_KEY, 'A "hash.key" byte buffer must be provided for ' +
285 `hash algorithm ${options.hash.algorithm}.`);
286 }
287 rounds = getNumberField(options.hash, 'rounds');
288 if (isNaN(rounds) || rounds <= 0 || rounds > 8) {
289 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_ROUNDS, 'A valid "hash.rounds" number between 1 and 8 must be provided for ' +
290 `hash algorithm ${options.hash.algorithm}.`);
291 }
292 const memoryCost = getNumberField(options.hash, 'memoryCost');
293 if (isNaN(memoryCost) || memoryCost <= 0 || memoryCost > 14) {
294 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_MEMORY_COST, 'A valid "hash.memoryCost" number between 1 and 14 must be provided for ' +
295 `hash algorithm ${options.hash.algorithm}.`);
296 }
297 if (typeof options.hash.saltSeparator !== 'undefined' &&
298 !validator.isBuffer(options.hash.saltSeparator)) {
299 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_SALT_SEPARATOR, '"hash.saltSeparator" must be a byte buffer.');
300 }
301 populatedOptions = {
302 hashAlgorithm: options.hash.algorithm,
303 signerKey: utils.toWebSafeBase64(options.hash.key),
304 rounds,
305 memoryCost,
306 saltSeparator: utils.toWebSafeBase64(options.hash.saltSeparator || Buffer.from('')),
307 };
308 break;
309 }
310 case 'BCRYPT':
311 populatedOptions = {
312 hashAlgorithm: options.hash.algorithm,
313 };
314 break;
315 case 'STANDARD_SCRYPT': {
316 const cpuMemCost = getNumberField(options.hash, 'memoryCost');
317 if (isNaN(cpuMemCost)) {
318 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_MEMORY_COST, 'A valid "hash.memoryCost" number must be provided for ' +
319 `hash algorithm ${options.hash.algorithm}.`);
320 }
321 const parallelization = getNumberField(options.hash, 'parallelization');
322 if (isNaN(parallelization)) {
323 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_PARALLELIZATION, 'A valid "hash.parallelization" number must be provided for ' +
324 `hash algorithm ${options.hash.algorithm}.`);
325 }
326 const blockSize = getNumberField(options.hash, 'blockSize');
327 if (isNaN(blockSize)) {
328 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_BLOCK_SIZE, 'A valid "hash.blockSize" number must be provided for ' +
329 `hash algorithm ${options.hash.algorithm}.`);
330 }
331 const dkLen = getNumberField(options.hash, 'derivedKeyLength');
332 if (isNaN(dkLen)) {
333 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_DERIVED_KEY_LENGTH, 'A valid "hash.derivedKeyLength" number must be provided for ' +
334 `hash algorithm ${options.hash.algorithm}.`);
335 }
336 populatedOptions = {
337 hashAlgorithm: options.hash.algorithm,
338 cpuMemCost,
339 parallelization,
340 blockSize,
341 dkLen,
342 };
343 break;
344 }
345 default:
346 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_ALGORITHM, `Unsupported hash algorithm provider "${options.hash.algorithm}".`);
347 }
348 return populatedOptions;
349 }
350 /**
351 * Validates and returns the users list of the uploadAccount request.
352 * Whenever a user with an error is detected, the error is cached and will later be
353 * merged into the user import result. This allows the processing of valid users without
354 * failing early on the first error detected.
355 * @param {UserImportRecord[]} users The UserImportRecords to convert to UnploadAccountUser
356 * objects.
357 * @param {ValidatorFunction=} userValidator The user validator function.
358 * @returns {UploadAccountUser[]} The populated uploadAccount users.
359 */
360 populateUsers(users, userValidator) {
361 const populatedUsers = [];
362 users.forEach((user, index) => {
363 try {
364 const result = populateUploadAccountUser(user, userValidator);
365 if (typeof result.passwordHash !== 'undefined') {
366 this.requiresHashOptions = true;
367 }
368 // Only users that pass client screening will be passed to backend for processing.
369 populatedUsers.push(result);
370 // Map user's index (the one to be sent to backend) to original developer provided array.
371 this.indexMap[populatedUsers.length - 1] = index;
372 }
373 catch (error) {
374 // Save the client side error with respect to the developer provided array.
375 this.userImportResultErrors.push({
376 index,
377 error,
378 });
379 }
380 });
381 return populatedUsers;
382 }
383}
384exports.UserImportBuilder = UserImportBuilder;