No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

accountImporter.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.serialImportUsers = exports.validateUserJson = exports.validateOptions = exports.transArrayToUser = void 0;
  4. const clc = require("colorette");
  5. const apiv2_1 = require("./apiv2");
  6. const api_1 = require("./api");
  7. const logger_1 = require("./logger");
  8. const error_1 = require("./error");
  9. const utils = require("./utils");
  10. const apiClient = new apiv2_1.Client({
  11. urlPrefix: api_1.googleOrigin,
  12. });
  13. const ALLOWED_JSON_KEYS = [
  14. "localId",
  15. "email",
  16. "emailVerified",
  17. "passwordHash",
  18. "salt",
  19. "displayName",
  20. "photoUrl",
  21. "createdAt",
  22. "lastSignedInAt",
  23. "providerUserInfo",
  24. "phoneNumber",
  25. "disabled",
  26. "customAttributes",
  27. ];
  28. const ALLOWED_JSON_KEYS_RENAMING = {
  29. lastSignedInAt: "lastLoginAt",
  30. };
  31. const ALLOWED_PROVIDER_USER_INFO_KEYS = ["providerId", "rawId", "email", "displayName", "photoUrl"];
  32. const ALLOWED_PROVIDER_IDS = ["google.com", "facebook.com", "twitter.com", "github.com"];
  33. function isValidBase64(str) {
  34. const expected = Buffer.from(str, "base64").toString("base64");
  35. if (str.length < expected.length && !str.endsWith("=")) {
  36. str += "=".repeat(expected.length - str.length);
  37. }
  38. return expected === str;
  39. }
  40. function toWebSafeBase64(data) {
  41. return data.replace(/\//g, "_").replace(/\+/g, "-");
  42. }
  43. function addProviderUserInfo(user, providerId, arr) {
  44. if (arr[0]) {
  45. user.providerUserInfo.push({
  46. providerId: providerId,
  47. rawId: arr[0],
  48. email: arr[1],
  49. displayName: arr[2],
  50. photoUrl: arr[3],
  51. });
  52. }
  53. }
  54. function genUploadAccountPostBody(projectId, accounts, hashOptions) {
  55. const postBody = {
  56. users: accounts.map((account) => {
  57. if (account.passwordHash) {
  58. account.passwordHash = toWebSafeBase64(account.passwordHash);
  59. }
  60. if (account.salt) {
  61. account.salt = toWebSafeBase64(account.salt);
  62. }
  63. for (const [key, value] of Object.entries(ALLOWED_JSON_KEYS_RENAMING)) {
  64. if (account[key]) {
  65. account[value] = account[key];
  66. delete account[key];
  67. }
  68. }
  69. return account;
  70. }),
  71. };
  72. if (hashOptions.hashAlgo) {
  73. postBody.hashAlgorithm = hashOptions.hashAlgo;
  74. }
  75. if (hashOptions.hashKey) {
  76. postBody.signerKey = toWebSafeBase64(hashOptions.hashKey);
  77. }
  78. if (hashOptions.saltSeparator) {
  79. postBody.saltSeparator = toWebSafeBase64(hashOptions.saltSeparator);
  80. }
  81. if (hashOptions.rounds) {
  82. postBody.rounds = hashOptions.rounds;
  83. }
  84. if (hashOptions.memCost) {
  85. postBody.memoryCost = hashOptions.memCost;
  86. }
  87. if (hashOptions.cpuMemCost) {
  88. postBody.cpuMemCost = hashOptions.cpuMemCost;
  89. }
  90. if (hashOptions.parallelization) {
  91. postBody.parallelization = hashOptions.parallelization;
  92. }
  93. if (hashOptions.blockSize) {
  94. postBody.blockSize = hashOptions.blockSize;
  95. }
  96. if (hashOptions.dkLen) {
  97. postBody.dkLen = hashOptions.dkLen;
  98. }
  99. if (hashOptions.passwordHashOrder) {
  100. postBody.passwordHashOrder = hashOptions.passwordHashOrder;
  101. }
  102. postBody.targetProjectId = projectId;
  103. return postBody;
  104. }
  105. function transArrayToUser(arr) {
  106. const user = {
  107. localId: arr[0],
  108. email: arr[1],
  109. emailVerified: arr[2] === "true",
  110. passwordHash: arr[3],
  111. salt: arr[4],
  112. displayName: arr[5],
  113. photoUrl: arr[6],
  114. createdAt: arr[23],
  115. lastLoginAt: arr[24],
  116. phoneNumber: arr[25],
  117. providerUserInfo: [],
  118. disabled: arr[26],
  119. customAttributes: arr[27],
  120. };
  121. addProviderUserInfo(user, "google.com", arr.slice(7, 11));
  122. addProviderUserInfo(user, "facebook.com", arr.slice(11, 15));
  123. addProviderUserInfo(user, "twitter.com", arr.slice(15, 19));
  124. addProviderUserInfo(user, "github.com", arr.slice(19, 23));
  125. if (user.passwordHash && !isValidBase64(user.passwordHash)) {
  126. return {
  127. error: "Password hash should be base64 encoded.",
  128. };
  129. }
  130. if (user.salt && !isValidBase64(user.salt)) {
  131. return {
  132. error: "Password salt should be base64 encoded.",
  133. };
  134. }
  135. return user;
  136. }
  137. exports.transArrayToUser = transArrayToUser;
  138. function validateOptions(options) {
  139. const hashOptions = validateRequiredParameters(options);
  140. if (!hashOptions.valid) {
  141. return hashOptions;
  142. }
  143. const hashInputOrder = options.hashInputOrder ? options.hashInputOrder.toUpperCase() : undefined;
  144. if (hashInputOrder) {
  145. if (hashInputOrder !== "SALT_FIRST" && hashInputOrder !== "PASSWORD_FIRST") {
  146. throw new error_1.FirebaseError("Unknown password hash order flag");
  147. }
  148. else {
  149. hashOptions["passwordHashOrder"] =
  150. hashInputOrder === "SALT_FIRST" ? "SALT_AND_PASSWORD" : "PASSWORD_AND_SALT";
  151. }
  152. }
  153. return hashOptions;
  154. }
  155. exports.validateOptions = validateOptions;
  156. function validateRequiredParameters(options) {
  157. if (!options.hashAlgo) {
  158. utils.logWarning("No hash algorithm specified. Password users cannot be imported.");
  159. return { valid: true };
  160. }
  161. const hashAlgo = options.hashAlgo.toUpperCase();
  162. let roundsNum;
  163. switch (hashAlgo) {
  164. case "HMAC_SHA512":
  165. case "HMAC_SHA256":
  166. case "HMAC_SHA1":
  167. case "HMAC_MD5":
  168. if (!options.hashKey || options.hashKey === "") {
  169. throw new error_1.FirebaseError("Must provide hash key(base64 encoded) for hash algorithm " + options.hashAlgo);
  170. }
  171. return { hashAlgo: hashAlgo, hashKey: options.hashKey, valid: true };
  172. case "MD5":
  173. case "SHA1":
  174. case "SHA256":
  175. case "SHA512":
  176. roundsNum = parseInt(options.rounds, 10);
  177. const minRounds = hashAlgo === "MD5" ? 0 : 1;
  178. if (isNaN(roundsNum) || roundsNum < minRounds || roundsNum > 8192) {
  179. throw new error_1.FirebaseError(`Must provide valid rounds(${minRounds}..8192) for hash algorithm ${options.hashAlgo}`);
  180. }
  181. return { hashAlgo: hashAlgo, rounds: options.rounds, valid: true };
  182. case "PBKDF_SHA1":
  183. case "PBKDF2_SHA256":
  184. roundsNum = parseInt(options.rounds, 10);
  185. if (isNaN(roundsNum) || roundsNum < 0 || roundsNum > 120000) {
  186. throw new error_1.FirebaseError("Must provide valid rounds(0..120000) for hash algorithm " + options.hashAlgo);
  187. }
  188. return { hashAlgo: hashAlgo, rounds: options.rounds, valid: true };
  189. case "SCRYPT":
  190. if (!options.hashKey || options.hashKey === "") {
  191. throw new error_1.FirebaseError("Must provide hash key(base64 encoded) for hash algorithm " + options.hashAlgo);
  192. }
  193. roundsNum = parseInt(options.rounds, 10);
  194. if (isNaN(roundsNum) || roundsNum <= 0 || roundsNum > 8) {
  195. throw new error_1.FirebaseError("Must provide valid rounds(1..8) for hash algorithm " + options.hashAlgo);
  196. }
  197. const memCost = parseInt(options.memCost, 10);
  198. if (isNaN(memCost) || memCost <= 0 || memCost > 14) {
  199. throw new error_1.FirebaseError("Must provide valid memory cost(1..14) for hash algorithm " + options.hashAlgo);
  200. }
  201. let saltSeparator = "";
  202. if (options.saltSeparator) {
  203. saltSeparator = options.saltSeparator;
  204. }
  205. return {
  206. hashAlgo: hashAlgo,
  207. hashKey: options.hashKey,
  208. saltSeparator: saltSeparator,
  209. rounds: options.rounds,
  210. memCost: options.memCost,
  211. valid: true,
  212. };
  213. case "BCRYPT":
  214. return { hashAlgo: hashAlgo, valid: true };
  215. case "STANDARD_SCRYPT":
  216. const cpuMemCost = parseInt(options.memCost, 10);
  217. const parallelization = parseInt(options.parallelization, 10);
  218. const blockSize = parseInt(options.blockSize, 10);
  219. const dkLen = parseInt(options.dkLen, 10);
  220. return {
  221. hashAlgo: hashAlgo,
  222. valid: true,
  223. cpuMemCost: cpuMemCost,
  224. parallelization: parallelization,
  225. blockSize: blockSize,
  226. dkLen: dkLen,
  227. };
  228. default:
  229. throw new error_1.FirebaseError("Unsupported hash algorithm " + clc.bold(options.hashAlgo));
  230. }
  231. }
  232. function validateProviderUserInfo(providerUserInfo) {
  233. if (!ALLOWED_PROVIDER_IDS.includes(providerUserInfo.providerId)) {
  234. return {
  235. error: JSON.stringify(providerUserInfo, null, 2) + " has unsupported providerId",
  236. };
  237. }
  238. const keydiff = Object.keys(providerUserInfo).filter((k) => !ALLOWED_PROVIDER_USER_INFO_KEYS.includes(k));
  239. if (keydiff.length) {
  240. return {
  241. error: JSON.stringify(providerUserInfo, null, 2) + " has unsupported keys: " + keydiff.join(","),
  242. };
  243. }
  244. return {};
  245. }
  246. function validateUserJson(userJson) {
  247. const keydiff = Object.keys(userJson).filter((k) => !ALLOWED_JSON_KEYS.includes(k));
  248. if (keydiff.length) {
  249. return {
  250. error: JSON.stringify(userJson, null, 2) + " has unsupported keys: " + keydiff.join(","),
  251. };
  252. }
  253. if (userJson.providerUserInfo) {
  254. for (let i = 0; i < userJson.providerUserInfo.length; i++) {
  255. const res = validateProviderUserInfo(userJson.providerUserInfo[i]);
  256. if (res.error) {
  257. return res;
  258. }
  259. }
  260. }
  261. const badFormat = JSON.stringify(userJson, null, 2) + " has invalid data format: ";
  262. if (userJson.passwordHash && !isValidBase64(userJson.passwordHash)) {
  263. return {
  264. error: badFormat + "Password hash should be base64 encoded.",
  265. };
  266. }
  267. if (userJson.salt && !isValidBase64(userJson.salt)) {
  268. return {
  269. error: badFormat + "Password salt should be base64 encoded.",
  270. };
  271. }
  272. return {};
  273. }
  274. exports.validateUserJson = validateUserJson;
  275. async function sendRequest(projectId, userList, hashOptions) {
  276. logger_1.logger.info("Starting importing " + userList.length + " account(s).");
  277. const postData = genUploadAccountPostBody(projectId, userList, hashOptions);
  278. return apiClient
  279. .post("/identitytoolkit/v3/relyingparty/uploadAccount", postData, {
  280. skipLog: { body: true },
  281. })
  282. .then((ret) => {
  283. if (ret.body.error) {
  284. logger_1.logger.info("Encountered problems while importing accounts. Details:");
  285. logger_1.logger.info(ret.body.error.map((rawInfo) => {
  286. return {
  287. account: JSON.stringify(userList[parseInt(rawInfo.index, 10)], null, 2),
  288. reason: rawInfo.message,
  289. };
  290. }));
  291. }
  292. else {
  293. utils.logSuccess("Imported successfully.");
  294. }
  295. logger_1.logger.info();
  296. });
  297. }
  298. function serialImportUsers(projectId, hashOptions, userListArr, index) {
  299. return sendRequest(projectId, userListArr[index], hashOptions).then(() => {
  300. if (index < userListArr.length - 1) {
  301. return serialImportUsers(projectId, hashOptions, userListArr, index + 1);
  302. }
  303. });
  304. }
  305. exports.serialImportUsers = serialImportUsers;