From 8d7cb6d911728caad996c9d792d422f522c970ae Mon Sep 17 00:00:00 2001 From: Florent Vilmart <364568+flovilmart@users.noreply.github.com> Date: Sun, 20 Jan 2019 17:22:23 -0500 Subject: [PATCH 1/2] Initial feature for totp --- package-lock.json | 177 +++++++----------- package.json | 1 + spec/.eslintrc.json | 3 +- spec/ParseUser.spec.js | 43 +++++ src/Adapters/Storage/Mongo/MongoTransform.js | 9 + .../Postgres/PostgresStorageAdapter.js | 5 + src/Controllers/DatabaseController.js | 3 + src/Routers/UsersRouter.js | 81 +++++++- 8 files changed, 206 insertions(+), 116 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8a135ab214..ebe87c2770 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1730,8 +1730,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -1749,13 +1748,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1768,18 +1765,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -1882,8 +1876,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -1893,7 +1886,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -1906,20 +1898,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.3.4", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -1927,13 +1916,11 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.0.2", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -1948,7 +1935,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -2021,8 +2007,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -2032,7 +2017,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -2108,8 +2092,7 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -2139,7 +2122,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2157,7 +2139,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2208,8 +2189,7 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -2242,7 +2222,7 @@ }, "bl": { "version": "1.2.2", - "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "dev": true, "requires": { @@ -2351,7 +2331,7 @@ }, "buffer": { "version": "4.9.1", - "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "requires": { "base64-js": "^1.0.2", @@ -2510,7 +2490,7 @@ }, "cheerio": { "version": "0.22.0", - "resolved": "http://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", "dev": true, "requires": { @@ -2597,7 +2577,7 @@ }, "cli-color": { "version": "0.3.2", - "resolved": "http://registry.npmjs.org/cli-color/-/cli-color-0.3.2.tgz", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-0.3.2.tgz", "integrity": "sha1-dfpfcowwjMSsWUsF4GzF2A2szYY=", "dev": true, "requires": { @@ -2824,7 +2804,7 @@ }, "css-select": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", "dev": true, "requires": { @@ -2847,7 +2827,7 @@ }, "d": { "version": "0.1.1", - "resolved": "http://registry.npmjs.org/d/-/d-0.1.1.tgz", + "resolved": "https://registry.npmjs.org/d/-/d-0.1.1.tgz", "integrity": "sha1-2hhMU10Y2O57oqoim5FACfrhEwk=", "dev": true, "requires": { @@ -3338,7 +3318,7 @@ "dependencies": { "d": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", "dev": true, "requires": { @@ -3372,7 +3352,7 @@ "dependencies": { "d": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", "dev": true, "requires": { @@ -3652,7 +3632,7 @@ "dependencies": { "d": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", "dev": true, "requires": { @@ -4230,8 +4210,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -4252,14 +4231,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4274,20 +4251,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -4404,8 +4378,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -4417,7 +4390,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4432,7 +4404,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4440,14 +4411,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4466,7 +4435,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -4547,8 +4515,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -4560,7 +4527,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -4646,8 +4612,7 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -4683,7 +4648,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4703,7 +4667,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4747,14 +4710,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -4844,7 +4805,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } @@ -4964,7 +4925,7 @@ }, "globby": { "version": "6.1.0", - "resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", "dev": true, "requires": { @@ -5478,7 +5439,7 @@ }, "is-builtin-module": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", "dev": true, "requires": { @@ -5630,7 +5591,7 @@ }, "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, @@ -5676,7 +5637,7 @@ }, "is-obj": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, @@ -6008,7 +5969,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } @@ -6619,7 +6580,7 @@ "dependencies": { "next-tick": { "version": "0.2.2", - "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-0.2.2.tgz", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-0.2.2.tgz", "integrity": "sha1-ddpKkn7liH45BliABltzNkE7MQ0=", "dev": true } @@ -6703,7 +6664,7 @@ }, "minimist": { "version": "0.0.8", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "mixin-deep": { @@ -6729,7 +6690,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" @@ -6757,7 +6718,7 @@ }, "mongodb-dbpath": { "version": "0.0.1", - "resolved": "http://registry.npmjs.org/mongodb-dbpath/-/mongodb-dbpath-0.0.1.tgz", + "resolved": "https://registry.npmjs.org/mongodb-dbpath/-/mongodb-dbpath-0.0.1.tgz", "integrity": "sha1-4BMsZ3sbncgwBFEW0Yrbf2kk8XU=", "dev": true, "requires": { @@ -6785,7 +6746,7 @@ }, "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, @@ -6832,7 +6793,7 @@ }, "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, @@ -6868,7 +6829,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } @@ -6909,7 +6870,7 @@ }, "mkdirp": { "version": "0.5.0", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", "dev": true, "requires": { @@ -6942,7 +6903,7 @@ }, "mongodb-version-list": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/mongodb-version-list/-/mongodb-version-list-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/mongodb-version-list/-/mongodb-version-list-1.0.0.tgz", "integrity": "sha1-8lAxz83W8UWx3o/OKk6+wCiLtKQ=", "dev": true, "requires": { @@ -6994,7 +6955,7 @@ }, "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, @@ -7084,7 +7045,7 @@ }, "next-tick": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, @@ -7134,7 +7095,6 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "dev": true, - "optional": true, "requires": { "remove-trailing-separator": "^1.0.1" } @@ -7248,7 +7208,6 @@ "version": "0.1.4", "bundled": true, "dev": true, - "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -7573,8 +7532,7 @@ "is-buffer": { "version": "1.1.6", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "is-builtin-module": { "version": "1.0.0", @@ -7658,7 +7616,6 @@ "version": "3.2.2", "bundled": true, "dev": true, - "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -7705,8 +7662,7 @@ "longest": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "lru-cache": { "version": "4.1.3", @@ -7972,8 +7928,7 @@ "repeat-string": { "version": "1.6.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "require-directory": { "version": "2.1.1", @@ -8456,7 +8411,7 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, @@ -8466,6 +8421,14 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, + "otplib": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/otplib/-/otplib-10.0.1.tgz", + "integrity": "sha512-FtbKelYtio2af5LDBWz3bWS6T03taHJAIv3evMrXuvoM50z5jbWoEMabPCk0A0JqiLGBzAIDJWfR9gSsvRYZHA==", + "requires": { + "thirty-two": "1.0.2" + } + }, "output-file-sync": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-2.0.1.tgz", @@ -9173,8 +9136,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true, - "optional": true + "dev": true }, "repeat-element": { "version": "1.1.3", @@ -9348,7 +9310,7 @@ }, "sax": { "version": "1.2.1", - "resolved": "http://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" }, "seek-bzip": { @@ -10046,14 +10008,19 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "thirty-two": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz", + "integrity": "sha1-TKL//AKlEpDSdEueP1V2k8prYno=" + }, "through": { "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { "version": "0.6.5", - "resolved": "http://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", "dev": true, "requires": { @@ -10280,7 +10247,7 @@ }, "buffer": { "version": "3.6.0", - "resolved": "http://registry.npmjs.org/buffer/-/buffer-3.6.0.tgz", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-3.6.0.tgz", "integrity": "sha1-pyyTb3e5a/UvX357RnGAYoVR3vs=", "dev": true, "requires": { @@ -10572,7 +10539,7 @@ }, "winston": { "version": "2.4.4", - "resolved": "http://registry.npmjs.org/winston/-/winston-2.4.4.tgz", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.4.tgz", "integrity": "sha512-NBo2Pepn4hK4V01UfcWcDlmiVTs7VTB1h7bgnB0rgP146bYhMxX0ypCz3lBOfNxCO4Zuek7yeT+y/zM1OfMw4Q==", "requires": { "async": "~1.0.0", diff --git a/package.json b/package.json index 06a103f5be..59b41cf335 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "lru-cache": "5.1.1", "mime": "2.4.0", "mongodb": "3.2.0", + "otplib": "^10.0.1", "parse": "2.1.0", "pg-promise": "8.5.5", "redis": "2.8.0", diff --git a/spec/.eslintrc.json b/spec/.eslintrc.json index 0814f305ce..be9f3842f8 100644 --- a/spec/.eslintrc.json +++ b/spec/.eslintrc.json @@ -23,7 +23,8 @@ "range": true, "jequal": true, "create": true, - "arrayContains": true + "arrayContains": true, + "expectAsync": true }, "rules": { "no-console": [0], diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 05429bf51b..7c778da6d4 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -12,6 +12,7 @@ const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageA const request = require('../lib/request'); const passwordCrypto = require('../lib/password'); const Config = require('../lib/Config'); +const otplib = require('otplib'); function verifyACL(user) { const ACL = user.getACL(); @@ -3754,3 +3755,45 @@ describe('Parse.User testing', () => { ); }); }); + +function enable2FA(user) { + return request({ + method: 'GET', + url: 'http://localhost:8378/1/users/me/enable2FA', + json: true, + headers: { + 'X-Parse-Session-Token': user.getSessionToken(), + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + }); +} + +function validate2FA(user, token) { + return request({ + method: 'POST', + url: 'http://localhost:8378/1/users/me/verify2FA', + body: { + token, + }, + headers: { + 'X-Parse-Session-Token': user.getSessionToken(), + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + }); +} + +describe('2FA', () => { + it('should enable 2FA tokens', async () => { + const user = await Parse.User.signUp('username', 'password'); + const { + data: { secret }, + } = await enable2FA(user); + const token = otplib.authenticator.generate(secret); + await validate2FA(user, token); + // await Parse.User.logOut(); + await expectAsync(Parse.User.logIn('username', 'password')).toBeRejected(); + }); +}); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 2ffefebd61..712d9f98ce 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -18,6 +18,8 @@ const transformKey = (className, fieldName, schema) => { return '_last_used'; case 'timesUsed': return 'times_used'; + case 'mfa': + return '_mfa'; } if ( @@ -106,6 +108,9 @@ const transformKeyValueForUpdate = ( key = 'times_used'; timeField = true; break; + case 'mfa': + key = '_mfa'; + break; } if ( @@ -286,6 +291,7 @@ function transformQueryKeyValue(className, key, value, schema) { case '_wperm': case '_perishable_token': case '_email_verify_token': + case '_mfa': return { key, value }; case '$or': case '$and': @@ -1349,6 +1355,9 @@ const mongoObjectToParseObject = (className, mongoObject, schema) => { break; case '_acl': break; + case '_mfa': + restObject._mfa = mongoObject[key]; + break; case '_email_verify_token': case '_perishable_token': case '_perishable_token_expires_at': diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index a24aad0e87..85b5597e41 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -951,6 +951,7 @@ export class PostgresStorageAdapter implements StorageAdapter { fields._perishable_token_expires_at = { type: 'Date' }; fields._password_changed_at = { type: 'Date' }; fields._password_history = { type: 'Array' }; + fields._mfa = { type: 'String' }; } let index = 2; const relations = []; @@ -1458,6 +1459,10 @@ export class PostgresStorageAdapter implements StorageAdapter { update['authData'] = update['authData'] || {}; update['authData'][provider] = value; } + if (fieldName === 'mfa') { + update['_mfa'] = update['mfa']; + delete update.mfa; + } } for (const fieldName in update) { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 0fdf55a876..7994fc467b 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -63,6 +63,7 @@ const specialQuerykeys = [ '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count', + '_mfa', ]; const isSpecialQueryKey = key => { @@ -184,6 +185,7 @@ const filterSensitiveData = (isMaster, aclGroup, className, object) => { delete object._account_lockout_expires_at; delete object._password_changed_at; delete object._password_history; + delete object._mfa; if (aclGroup.indexOf(object.objectId) > -1) { return object; @@ -212,6 +214,7 @@ const specialKeysForUpdate = [ '_perishable_token_expires_at', '_password_changed_at', '_password_history', + '_mfa', ]; const isSpecialUpdateKey = key => { diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index b7c02fa2f5..31588541ab 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -7,6 +7,7 @@ import ClassesRouter from './ClassesRouter'; import rest from '../rest'; import Auth from '../Auth'; import passwordCrypto from '../password'; +import * as otplib from 'otplib'; export class UsersRouter extends ClassesRouter { className() { @@ -44,7 +45,7 @@ export class UsersRouter extends ClassesRouter { ) { payload = req.query; } - const { username, email, password } = payload; + const { username, email, password, token } = payload; // TODO: use the right error codes / descriptions. if (!username && !email) { @@ -153,7 +154,12 @@ export class UsersRouter extends ClassesRouter { delete user.authData; } } - + if (user._mfa) { + if (!otplib.authenticator.verify({ token, secret: user._mfa })) { + throw new Parse.Error(-1, 'Invalid 2FA token'); + } + } + delete user._mfa; return resolve(user); }) .catch(error => { @@ -189,16 +195,15 @@ export class UsersRouter extends ClassesRouter { Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token' ); - } else { - const user = response.results[0].user; - // Send token back on the login, because SDKs expect that. - user.sessionToken = sessionToken; + } + const user = response.results[0].user; + // Send token back on the login, because SDKs expect that. + user.sessionToken = sessionToken; - // Remove hidden properties. - UsersRouter.removeHiddenProperties(user); + // Remove hidden properties. + UsersRouter.removeHiddenProperties(user); - return { response: user }; - } + return { response: user }; }); } @@ -310,6 +315,60 @@ export class UsersRouter extends ClassesRouter { return Promise.resolve(success); } + async enable2FA(req) { + const { user } = req.auth; + const secret = otplib.authenticator.generateSecret(); + const otpauth = otplib.authenticator.keyuri( + user.username, + 'service', + secret + ); + await rest.update( + req.config, + req.auth, + '_User', + { + objectId: user.id, + }, + { + mfa: `pending:${secret}`, + } + ); + return { response: { qrcodeURL: otpauth, secret } }; + } + + async verify2FA(req) { + const { token } = req.body; + // Fetch the user directly from the DB as we need the _mfa + const [user] = await req.config.database.find('_User', { + objectId: req.auth.user.id, + }); + const mfa = user._mfa; + if (!mfa) { + throw new Parse.Error(-1, 'MFA is not enabled on this account'); + } + if (mfa.indexOf('pending:') !== 0) { + throw new Parse.Error(-1, 'MFA is already active'); + } + const secret = mfa.slice('pending:'.length); + const result = otplib.authenticator.verify({ token, secret }); + if (!result) { + throw new Parse.Error(-1, 'Invalid token'); + } + await rest.update( + req.config, + req.auth, + '_User', + { + objectId: req.auth.user.id, + }, + { + mfa: `${secret}`, + } + ); + return { response: {} }; + } + _throwOnBadEmailConfig(req) { try { Config.validateEmailConfiguration({ @@ -441,6 +500,8 @@ export class UsersRouter extends ClassesRouter { this.route('POST', '/logout', req => { return this.handleLogOut(req); }); + this.route('GET', '/users/me/enable2FA', req => this.enable2FA(req)); + this.route('POST', '/users/me/verify2FA', req => this.verify2FA(req)); this.route('POST', '/requestPasswordReset', req => { return this.handleResetRequest(req); }); From b395f15a2d99bcb783ba5a4ef377fd33d5253232 Mon Sep 17 00:00:00 2001 From: Florent Vilmart <364568+flovilmart@users.noreply.github.com> Date: Sun, 20 Jan 2019 17:48:55 -0500 Subject: [PATCH 2/2] Remove usage of expectAsync --- spec/.eslintrc.json | 3 +-- spec/ParseUser.spec.js | 10 ++++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/spec/.eslintrc.json b/spec/.eslintrc.json index be9f3842f8..0814f305ce 100644 --- a/spec/.eslintrc.json +++ b/spec/.eslintrc.json @@ -23,8 +23,7 @@ "range": true, "jequal": true, "create": true, - "arrayContains": true, - "expectAsync": true + "arrayContains": true }, "rules": { "no-console": [0], diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 7c778da6d4..0ab92e3823 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -3793,7 +3793,13 @@ describe('2FA', () => { } = await enable2FA(user); const token = otplib.authenticator.generate(secret); await validate2FA(user, token); - // await Parse.User.logOut(); - await expectAsync(Parse.User.logIn('username', 'password')).toBeRejected(); + await Parse.User.logOut(); + await Parse.User.logIn('username', 'password') + .then(() => { + throw 'Login should have failed'; + }) + .catch(() => { + /**/ + }); }); });