UNPKG

18.8 kBJavaScriptView Raw
1/*! firebase-admin v10.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;
20var deep_copy_1 = require("../utils/deep-copy");
21var utils = require("../utils");
22var validator = require("../utils/validator");
23var 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 var 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 var authFactorInfo = {
45 mfaEnrollmentId: multiFactorInfo.uid,
46 displayName: multiFactorInfo.displayName,
47 // Required for all phone second factors.
48 phoneInfo: multiFactorInfo.phoneNumber,
49 enrolledAt: enrolledAt,
50 };
51 for (var 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 var 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(function (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(function (multiFactorInfo) {
134 result.mfaInfo.push(convertMultiFactorInfoToServerFormat(multiFactorInfo));
135 });
136 }
137 // Remove blank fields.
138 var 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 */
161var UserImportBuilder = /** @class */ (function () {
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 function UserImportBuilder(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 UserImportBuilder.prototype.buildRequest = function () {
182 var users = this.validatedUsers.map(function (user) {
183 return deep_copy_1.deepCopy(user);
184 });
185 return deep_copy_1.deepExtend({ users: users }, 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 UserImportBuilder.prototype.buildResponse = function (failedUploads) {
194 var _this = this;
195 // Initialize user import result.
196 var importResult = {
197 successCount: this.validatedUsers.length,
198 failureCount: this.userImportResultErrors.length,
199 errors: deep_copy_1.deepCopy(this.userImportResultErrors),
200 };
201 importResult.failureCount += failedUploads.length;
202 importResult.successCount -= failedUploads.length;
203 failedUploads.forEach(function (failedUpload) {
204 importResult.errors.push({
205 // Map backend request index to original developer provided array index.
206 index: _this.indexMap[failedUpload.index],
207 error: new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_USER_IMPORT, failedUpload.message),
208 });
209 });
210 // Sort errors by index.
211 importResult.errors.sort(function (a, b) {
212 return a.index - b.index;
213 });
214 // Return sorted result.
215 return importResult;
216 };
217 /**
218 * Validates and returns the hashing options of the uploadAccount request.
219 * Throws an error whenever an invalid or missing options is detected.
220 * @param {UserImportOptions} options The UserImportOptions.
221 * @param {boolean} requiresHashOptions Whether to require hash options.
222 * @returns {UploadAccountOptions} The populated UploadAccount options.
223 */
224 UserImportBuilder.prototype.populateOptions = function (options, requiresHashOptions) {
225 var populatedOptions;
226 if (!requiresHashOptions) {
227 return {};
228 }
229 if (!validator.isNonNullObject(options)) {
230 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, '"UserImportOptions" are required when importing users with passwords.');
231 }
232 if (!validator.isNonNullObject(options.hash)) {
233 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.MISSING_HASH_ALGORITHM, '"hash.algorithm" is missing from the provided "UserImportOptions".');
234 }
235 if (typeof options.hash.algorithm === 'undefined' ||
236 !validator.isNonEmptyString(options.hash.algorithm)) {
237 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_ALGORITHM, '"hash.algorithm" must be a string matching the list of supported algorithms.');
238 }
239 var rounds;
240 switch (options.hash.algorithm) {
241 case 'HMAC_SHA512':
242 case 'HMAC_SHA256':
243 case 'HMAC_SHA1':
244 case 'HMAC_MD5':
245 if (!validator.isBuffer(options.hash.key)) {
246 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_KEY, 'A non-empty "hash.key" byte buffer must be provided for ' +
247 ("hash algorithm " + options.hash.algorithm + "."));
248 }
249 populatedOptions = {
250 hashAlgorithm: options.hash.algorithm,
251 signerKey: utils.toWebSafeBase64(options.hash.key),
252 };
253 break;
254 case 'MD5':
255 case 'SHA1':
256 case 'SHA256':
257 case 'SHA512': {
258 // MD5 is [0,8192] but SHA1, SHA256, and SHA512 are [1,8192]
259 rounds = getNumberField(options.hash, 'rounds');
260 var minRounds = options.hash.algorithm === 'MD5' ? 0 : 1;
261 if (isNaN(rounds) || rounds < minRounds || rounds > 8192) {
262 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_ROUNDS, "A valid \"hash.rounds\" number between " + minRounds + " and 8192 must be provided for " +
263 ("hash algorithm " + options.hash.algorithm + "."));
264 }
265 populatedOptions = {
266 hashAlgorithm: options.hash.algorithm,
267 rounds: rounds,
268 };
269 break;
270 }
271 case 'PBKDF_SHA1':
272 case 'PBKDF2_SHA256':
273 rounds = getNumberField(options.hash, 'rounds');
274 if (isNaN(rounds) || rounds < 0 || rounds > 120000) {
275 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_ROUNDS, 'A valid "hash.rounds" number between 0 and 120000 must be provided for ' +
276 ("hash algorithm " + options.hash.algorithm + "."));
277 }
278 populatedOptions = {
279 hashAlgorithm: options.hash.algorithm,
280 rounds: rounds,
281 };
282 break;
283 case 'SCRYPT': {
284 if (!validator.isBuffer(options.hash.key)) {
285 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_KEY, 'A "hash.key" byte buffer must be provided for ' +
286 ("hash algorithm " + options.hash.algorithm + "."));
287 }
288 rounds = getNumberField(options.hash, 'rounds');
289 if (isNaN(rounds) || rounds <= 0 || rounds > 8) {
290 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_ROUNDS, 'A valid "hash.rounds" number between 1 and 8 must be provided for ' +
291 ("hash algorithm " + options.hash.algorithm + "."));
292 }
293 var memoryCost = getNumberField(options.hash, 'memoryCost');
294 if (isNaN(memoryCost) || memoryCost <= 0 || memoryCost > 14) {
295 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 ' +
296 ("hash algorithm " + options.hash.algorithm + "."));
297 }
298 if (typeof options.hash.saltSeparator !== 'undefined' &&
299 !validator.isBuffer(options.hash.saltSeparator)) {
300 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_SALT_SEPARATOR, '"hash.saltSeparator" must be a byte buffer.');
301 }
302 populatedOptions = {
303 hashAlgorithm: options.hash.algorithm,
304 signerKey: utils.toWebSafeBase64(options.hash.key),
305 rounds: rounds,
306 memoryCost: memoryCost,
307 saltSeparator: utils.toWebSafeBase64(options.hash.saltSeparator || Buffer.from('')),
308 };
309 break;
310 }
311 case 'BCRYPT':
312 populatedOptions = {
313 hashAlgorithm: options.hash.algorithm,
314 };
315 break;
316 case 'STANDARD_SCRYPT': {
317 var cpuMemCost = getNumberField(options.hash, 'memoryCost');
318 if (isNaN(cpuMemCost)) {
319 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_MEMORY_COST, 'A valid "hash.memoryCost" number must be provided for ' +
320 ("hash algorithm " + options.hash.algorithm + "."));
321 }
322 var parallelization = getNumberField(options.hash, 'parallelization');
323 if (isNaN(parallelization)) {
324 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_PARALLELIZATION, 'A valid "hash.parallelization" number must be provided for ' +
325 ("hash algorithm " + options.hash.algorithm + "."));
326 }
327 var blockSize = getNumberField(options.hash, 'blockSize');
328 if (isNaN(blockSize)) {
329 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_BLOCK_SIZE, 'A valid "hash.blockSize" number must be provided for ' +
330 ("hash algorithm " + options.hash.algorithm + "."));
331 }
332 var dkLen = getNumberField(options.hash, 'derivedKeyLength');
333 if (isNaN(dkLen)) {
334 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_DERIVED_KEY_LENGTH, 'A valid "hash.derivedKeyLength" number must be provided for ' +
335 ("hash algorithm " + options.hash.algorithm + "."));
336 }
337 populatedOptions = {
338 hashAlgorithm: options.hash.algorithm,
339 cpuMemCost: cpuMemCost,
340 parallelization: parallelization,
341 blockSize: blockSize,
342 dkLen: dkLen,
343 };
344 break;
345 }
346 default:
347 throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_HASH_ALGORITHM, "Unsupported hash algorithm provider \"" + options.hash.algorithm + "\".");
348 }
349 return populatedOptions;
350 };
351 /**
352 * Validates and returns the users list of the uploadAccount request.
353 * Whenever a user with an error is detected, the error is cached and will later be
354 * merged into the user import result. This allows the processing of valid users without
355 * failing early on the first error detected.
356 * @param {UserImportRecord[]} users The UserImportRecords to convert to UnploadAccountUser
357 * objects.
358 * @param {ValidatorFunction=} userValidator The user validator function.
359 * @returns {UploadAccountUser[]} The populated uploadAccount users.
360 */
361 UserImportBuilder.prototype.populateUsers = function (users, userValidator) {
362 var _this = this;
363 var populatedUsers = [];
364 users.forEach(function (user, index) {
365 try {
366 var result = populateUploadAccountUser(user, userValidator);
367 if (typeof result.passwordHash !== 'undefined') {
368 _this.requiresHashOptions = true;
369 }
370 // Only users that pass client screening will be passed to backend for processing.
371 populatedUsers.push(result);
372 // Map user's index (the one to be sent to backend) to original developer provided array.
373 _this.indexMap[populatedUsers.length - 1] = index;
374 }
375 catch (error) {
376 // Save the client side error with respect to the developer provided array.
377 _this.userImportResultErrors.push({
378 index: index,
379 error: error,
380 });
381 }
382 });
383 return populatedUsers;
384 };
385 return UserImportBuilder;
386}());
387exports.UserImportBuilder = UserImportBuilder;