user-import-builder.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. /*! firebase-admin v12.1.1 */
  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. */
  18. Object.defineProperty(exports, "__esModule", { value: true });
  19. exports.UserImportBuilder = exports.convertMultiFactorInfoToServerFormat = void 0;
  20. const deep_copy_1 = require("../utils/deep-copy");
  21. const utils = require("../utils");
  22. const validator = require("../utils/validator");
  23. const 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. */
  29. function 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. }
  63. exports.convertMultiFactorInfoToServerFormat = convertMultiFactorInfoToServerFormat;
  64. function 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. */
  72. function 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. */
  85. function 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. */
  161. class 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. }
  384. exports.UserImportBuilder = UserImportBuilder;