From d869968f8817b7baf88edcc4761dcabd747870f3 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 25 Dec 2022 12:45:43 -0500 Subject: [PATCH 1/6] Begin api-key functionality on backend --- backend/package-lock.json | 522 +++++++++++++++++- backend/package.json | 3 + backend/src/app.ts | 5 +- .../src/controllers/serviceTokenController.ts | 9 + backend/src/models/apiKey.ts | 63 +++ backend/src/models/index.ts | 5 +- backend/src/routes/apiKey.ts | 80 +++ backend/src/routes/index.ts | 4 +- backend/src/routes/serviceToken.ts | 2 +- 9 files changed, 658 insertions(+), 35 deletions(-) create mode 100644 backend/src/models/apiKey.ts create mode 100644 backend/src/routes/apiKey.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 85b168c1e..89d371e2a 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,6 +16,7 @@ "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", "axios": "^1.1.3", + "bcrypt": "^5.1.0", "bigint-conversion": "^2.2.2", "cookie-parser": "^1.4.6", "cors": "^2.8.5", @@ -44,6 +45,8 @@ "devDependencies": { "@jest/globals": "^29.3.1", "@posthog/plugin-scaffold": "^1.3.4", + "@types/bcrypt": "^5.0.0", + "@types/bcryptjs": "^2.4.2", "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", "@types/express": "^4.17.14", @@ -2576,6 +2579,39 @@ "resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.0.5.tgz", "integrity": "sha512-gTIElNo4ohMcYUZzol/Hb6DYJzphxl0b1B4egJJ+JiqxqcOcWx4XLMAB+lhWuMsMX3uR1oc5hwPusU3lgc1FkQ==" }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", + "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@maxmind/geoip2-node": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-3.5.0.tgz", @@ -3033,6 +3069,21 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz", + "integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==", + "dev": true + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -3483,8 +3534,7 @@ "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "node_modules/accepts": { "version": "1.3.8", @@ -3586,7 +3636,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -3619,6 +3668,23 @@ "node": ">= 8" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -3803,6 +3869,19 @@ } ] }, + "node_modules/bcrypt": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", + "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.10", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -4132,6 +4211,14 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/ci-info": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", @@ -4218,6 +4305,14 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/color/node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -4262,6 +4357,11 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -4452,6 +4552,11 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -4482,6 +4587,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -4585,8 +4698,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/enabled": { "version": "2.0.0", @@ -4601,6 +4713,29 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5259,6 +5394,28 @@ "node": ">= 0.6" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -5283,6 +5440,25 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5476,6 +5652,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "node_modules/hash-base": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", @@ -5726,7 +5907,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -6696,7 +6876,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, "dependencies": { "semver": "^6.0.0" }, @@ -6711,7 +6890,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -6880,11 +7058,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.0.0.tgz", + "integrity": "sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -7004,6 +7215,11 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/node-addon-api": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", + "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==" + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -9731,6 +9947,17 @@ "inBundle": true, "license": "ISC" }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -10514,6 +10741,11 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -10573,8 +10805,7 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/simple-swizzle": { "version": "0.2.2", @@ -10795,7 +11026,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -10809,7 +11039,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -10935,6 +11164,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^4.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -11403,6 +11648,14 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/winston": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", @@ -13768,6 +14021,32 @@ "resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.0.5.tgz", "integrity": "sha512-gTIElNo4ohMcYUZzol/Hb6DYJzphxl0b1B4egJJ+JiqxqcOcWx4XLMAB+lhWuMsMX3uR1oc5hwPusU3lgc1FkQ==" }, + "@mapbox/node-pre-gyp": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", + "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", + "requires": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "dependencies": { + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "requires": { + "abbrev": "1" + } + } + } + }, "@maxmind/geoip2-node": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-3.5.0.tgz", @@ -14152,6 +14431,21 @@ "@babel/types": "^7.3.0" } }, + "@types/bcrypt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz", + "integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==", + "dev": true + }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -14513,8 +14807,7 @@ "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "accepts": { "version": "1.3.8", @@ -14584,8 +14877,7 @@ "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "ansi-styles": { "version": "4.3.0", @@ -14606,6 +14898,20 @@ "picomatch": "^2.0.4" } }, + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -14746,6 +15052,15 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "bcrypt": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", + "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==", + "requires": { + "@mapbox/node-pre-gyp": "^1.0.10", + "node-addon-api": "^5.0.0" + } + }, "before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -14973,6 +15288,11 @@ } } }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, "ci-info": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", @@ -15064,6 +15384,11 @@ "simple-swizzle": "^0.2.2" } }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" + }, "colorspace": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", @@ -15092,6 +15417,11 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -15237,6 +15567,11 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -15257,6 +15592,11 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, + "detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -15336,8 +15676,7 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "enabled": { "version": "2.0.0", @@ -15349,6 +15688,28 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "peer": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "peer": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -15856,6 +16217,24 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -15873,6 +16252,22 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -16003,6 +16398,11 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "hash-base": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", @@ -16181,8 +16581,7 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-generator-fn": { "version": "2.1.0", @@ -16944,7 +17343,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, "requires": { "semver": "^6.0.0" }, @@ -16952,8 +17350,7 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, @@ -17078,11 +17475,37 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" }, + "minipass": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.0.0.tgz", + "integrity": "sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, "mmdb-lib": { "version": "2.0.2", @@ -17173,6 +17596,11 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node-addon-api": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", + "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==" + }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -19082,6 +19510,17 @@ "path-key": "^3.0.0" } }, + "npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "requires": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -19637,6 +20076,11 @@ "send": "0.18.0" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -19684,8 +20128,7 @@ "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "simple-swizzle": { "version": "0.2.2", @@ -19860,7 +20303,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -19871,7 +20313,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -19960,6 +20401,19 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^4.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -20277,6 +20731,14 @@ "isexe": "^2.0.0" } }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "winston": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", diff --git a/backend/package.json b/backend/package.json index d1a03a74c..174e7add5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,6 +7,7 @@ "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", "axios": "^1.1.3", + "bcrypt": "^5.1.0", "bigint-conversion": "^2.2.2", "cookie-parser": "^1.4.6", "cors": "^2.8.5", @@ -62,6 +63,8 @@ "devDependencies": { "@jest/globals": "^29.3.1", "@posthog/plugin-scaffold": "^1.3.4", + "@types/bcrypt": "^5.0.0", + "@types/bcryptjs": "^2.4.2", "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", "@types/express": "^4.17.14", diff --git a/backend/src/app.ts b/backend/src/app.ts index e49551a91..6358293ca 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -27,7 +27,8 @@ import { password as passwordRouter, stripe as stripeRouter, integration as integrationRouter, - integrationAuth as integrationAuthRouter + integrationAuth as integrationAuthRouter, + apiKey as apiKeyRouter } from './routes'; import { getLogger } from './utils/logger'; import { RouteNotFoundError } from './utils/errors'; @@ -74,7 +75,7 @@ app.use('/api/v1/password', passwordRouter); app.use('/api/v1/stripe', stripeRouter); app.use('/api/v1/integration', integrationRouter); app.use('/api/v1/integration-auth', integrationAuthRouter); - +app.use('/api/v1/api-key', apiKeyRouter); //* Handle unrouted requests and respond with proper error message as well as status code app.use((req, res, next)=>{ diff --git a/backend/src/controllers/serviceTokenController.ts b/backend/src/controllers/serviceTokenController.ts index 4cc53c4f9..43a48b558 100644 --- a/backend/src/controllers/serviceTokenController.ts +++ b/backend/src/controllers/serviceTokenController.ts @@ -74,3 +74,12 @@ export const createServiceToken = async (req: Request, res: Response) => { token }); }; + +/** + * SERVICE_TOKEN: , + * - authorizes the service token for "service token"-only endpoints. + * - authorizes the service token to pull secrets via that endpoint. + * + * + * + */ \ No newline at end of file diff --git a/backend/src/models/apiKey.ts b/backend/src/models/apiKey.ts new file mode 100644 index 000000000..488b24694 --- /dev/null +++ b/backend/src/models/apiKey.ts @@ -0,0 +1,63 @@ +import { Schema, model, Types } from 'mongoose'; +import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; + +// TODO: add scopes + +export interface IAPIKey { + name: string; + workspace: string; + environment: string; + expiresAt: Date; + prefix: string; + apiKeyHash: string; + encryptedKey: string; + iv: string; + tag: string; +} + +const apiKeySchema = new Schema( + { + name: { + type: String, + required: true + }, + workspace: { + type: String + }, + environment: { + type: String, + enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD] + }, + expiresAt: { + type: Date + }, + prefix: { + type: String, + required: true + }, + apiKeyHash: { + type: String, + unique: true, + required: true + }, + encryptedKey: { + type: String, + select: true + }, + iv: { + type: String, + select: true + }, + tag: { + type: String, + select: true + } + }, + { + timestamps: true + } +); + +const APIKey = model('APIKey', apiKeySchema); + +export default APIKey; diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 78c38060b..0c40f155d 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -14,6 +14,7 @@ import Token, { IToken } from './token'; import User, { IUser } from './user'; import UserAction, { IUserAction } from './userAction'; import Workspace, { IWorkspace } from './workspace'; +import APIKey, { IAPIKey } from './apiKey'; export { BackupPrivateKey, @@ -47,5 +48,7 @@ export { UserAction, IUserAction, Workspace, - IWorkspace + IWorkspace, + APIKey, + IAPIKey, }; diff --git a/backend/src/routes/apiKey.ts b/backend/src/routes/apiKey.ts new file mode 100644 index 000000000..c5fc7dd5d --- /dev/null +++ b/backend/src/routes/apiKey.ts @@ -0,0 +1,80 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth +} from '../middleware'; +import { + APIKey +} from '../models'; +import { body } from 'express-validator'; +import crypto from 'crypto'; +import bcrypt from 'bcrypt'; +// import * as bcrypt from 'bcrypt'; +// const bcrypt = require('bcrypt'); +import * as Sentry from '@sentry/node'; + +// POST /api/v1/api-key +router.post( + '/', + requireAuth, + body('name').exists().trim(), + body('workspace'), + body('environment'), + body('encryptedKey'), + body('iv'), + body('tag'), + body('expiresAt'), + async (req, res) => { + let savedAPIKey; + try { + const { + name, + workspace, + environment, + encryptedKey, + iv, + tag, + expiresAt + } = req.body; + + // api-key: 38 characters + // 6-char: prefix + // 32-char: remaining + const apiKey = crypto.randomBytes(19).toString('hex'); + const saltRounds = 10; // config? + const apiKeyHash = await bcrypt.hash(apiKey, saltRounds); + + savedAPIKey = await new APIKey({ + name, + workspace, + environment, + expiresAt, + prefix: apiKey.substring(0, 6), + apiKeyHash, + encryptedKey, + iv, + tag + }).save(); + + // 1. generate api key + // 2. hash api key with bcrypt + // 3. store hash and api key info in db + // 4. return api key + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'xxx' + }); + } + + return res.status(200).send({ + apiKey: savedAPIKey + }); + } +); + +// INFISICAL TOKEN = . + +export default router; \ No newline at end of file diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 2dfe58baa..bc0ff776d 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -15,6 +15,7 @@ import password from './password'; import stripe from './stripe'; import integration from './integration'; import integrationAuth from './integrationAuth'; +import apiKey from './apiKey'; export { signup, @@ -33,5 +34,6 @@ export { password, stripe, integration, - integrationAuth + integrationAuth, + apiKey }; diff --git a/backend/src/routes/serviceToken.ts b/backend/src/routes/serviceToken.ts index 00195edee..f9452db63 100644 --- a/backend/src/routes/serviceToken.ts +++ b/backend/src/routes/serviceToken.ts @@ -10,7 +10,7 @@ import { body } from 'express-validator'; import { ADMIN, MEMBER, GRANTED } from '../variables'; import { serviceTokenController } from '../controllers'; -// TODO: revoke service token +// Note to devs: service-token to be deprecated in favor of api-key router.get( '/', From 888d28d6b9dcdb184b406826e980e00c04d27b28 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 25 Dec 2022 19:19:56 -0500 Subject: [PATCH 2/6] Continue work on API key --- backend/Dockerfile | 5 +- backend/package.json | 1 - .../src/middleware/requireAPIKeyDataAuth.ts | 40 +++++++++ .../src/models/{apiKey.ts => apiKeyData.ts} | 33 +++---- backend/src/models/index.ts | 6 +- backend/src/routes/apiKey.ts | 85 ++++++++++++++----- 6 files changed, 131 insertions(+), 39 deletions(-) create mode 100644 backend/src/middleware/requireAPIKeyDataAuth.ts rename backend/src/models/{apiKey.ts => apiKeyData.ts} (61%) diff --git a/backend/Dockerfile b/backend/Dockerfile index 85b7204fe..e4d283a77 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -4,7 +4,10 @@ WORKDIR /app COPY package.json package-lock.json ./ -RUN npm ci --only-production --ignore-scripts +# RUN npm ci --only-production --ignore-scripts +# "prepare": "cd .. && npm install" + +RUN npm ci --only-production COPY . . diff --git a/backend/package.json b/backend/package.json index 174e7add5..0bcb36e49 100644 --- a/backend/package.json +++ b/backend/package.json @@ -37,7 +37,6 @@ "version": "1.0.0", "main": "src/index.js", "scripts": { - "prepare": "cd .. && npm install", "start": "npm run build && node build/index.js", "dev": "nodemon", "build": "rimraf ./build && tsc && cp -R ./src/templates ./build", diff --git a/backend/src/middleware/requireAPIKeyDataAuth.ts b/backend/src/middleware/requireAPIKeyDataAuth.ts new file mode 100644 index 000000000..8dafb5a9c --- /dev/null +++ b/backend/src/middleware/requireAPIKeyDataAuth.ts @@ -0,0 +1,40 @@ +import { Request, Response, NextFunction } from 'express'; +import { APIKeyData } from '../models'; +import { validateMembership } from '../helpers/membership'; +import { AccountNotFoundError } from '../utils/errors'; + +type req = 'params' | 'body' | 'query'; + +const requireAPIKeyDataAuth = ({ + acceptedRoles, + acceptedStatuses, + location = 'params' +}: { + acceptedRoles: string[]; + acceptedStatuses: string[]; + location?: req; +}) => { + return async (req: Request, res: Response, next: NextFunction) => { + + // req.user + + const apiKeyData = await APIKeyData.findById(req[location].apiKeyDataId); + + if (!apiKeyData) { + return next(AccountNotFoundError({message: 'Failed to locate API Key data'})); + } + + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: apiKeyData?.workspace.toString(), + acceptedRoles, + acceptedStatuses + }); + + req.apiKeyData = '' // ?? + + next(); + } +} + +export default requireAPIKeyDataAuth; \ No newline at end of file diff --git a/backend/src/models/apiKey.ts b/backend/src/models/apiKeyData.ts similarity index 61% rename from backend/src/models/apiKey.ts rename to backend/src/models/apiKeyData.ts index 488b24694..8b064bf08 100644 --- a/backend/src/models/apiKey.ts +++ b/backend/src/models/apiKeyData.ts @@ -1,12 +1,12 @@ import { Schema, model, Types } from 'mongoose'; import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; -// TODO: add scopes - -export interface IAPIKey { +export interface IAPIKeyData { name: string; - workspace: string; - environment: string; + workspaces: { + workspace: Types.ObjectId, + environments: string[] + }[]; expiresAt: Date; prefix: string; apiKeyHash: string; @@ -15,19 +15,22 @@ export interface IAPIKey { tag: string; } -const apiKeySchema = new Schema( +const apiKeyDataSchema = new Schema( { name: { type: String, required: true }, - workspace: { - type: String - }, - environment: { - type: String, - enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD] - }, + workspaces: [{ + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace' + }, + environments: [{ + type: String, + enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD] + }] + }], expiresAt: { type: Date }, @@ -58,6 +61,6 @@ const apiKeySchema = new Schema( } ); -const APIKey = model('APIKey', apiKeySchema); +const APIKeyData = model('APIKeyData', apiKeyDataSchema); -export default APIKey; +export default APIKeyData; diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 0c40f155d..fd9578523 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -14,7 +14,7 @@ import Token, { IToken } from './token'; import User, { IUser } from './user'; import UserAction, { IUserAction } from './userAction'; import Workspace, { IWorkspace } from './workspace'; -import APIKey, { IAPIKey } from './apiKey'; +import APIKeyData, { IAPIKeyData } from './apiKeyData'; export { BackupPrivateKey, @@ -49,6 +49,6 @@ export { IUserAction, Workspace, IWorkspace, - APIKey, - IAPIKey, + APIKeyData, + IAPIKeyData, }; diff --git a/backend/src/routes/apiKey.ts b/backend/src/routes/apiKey.ts index c5fc7dd5d..a40c852ac 100644 --- a/backend/src/routes/apiKey.ts +++ b/backend/src/routes/apiKey.ts @@ -4,16 +4,14 @@ import { requireAuth } from '../middleware'; import { - APIKey + APIKeyData } from '../models'; -import { body } from 'express-validator'; +import { param, body, query } from 'express-validator'; import crypto from 'crypto'; import bcrypt from 'bcrypt'; -// import * as bcrypt from 'bcrypt'; -// const bcrypt = require('bcrypt'); import * as Sentry from '@sentry/node'; -// POST /api/v1/api-key +// TODO: middleware router.post( '/', requireAuth, @@ -25,7 +23,7 @@ router.post( body('tag'), body('expiresAt'), async (req, res) => { - let savedAPIKey; + let apiKey, apiKeyData; try { const { name, @@ -37,14 +35,13 @@ router.post( expiresAt } = req.body; - // api-key: 38 characters - // 6-char: prefix - // 32-char: remaining - const apiKey = crypto.randomBytes(19).toString('hex'); - const saltRounds = 10; // config? + // create 38-char API key with first 6-char being the prefix + apiKey = crypto.randomBytes(19).toString('hex'); + + const saltRounds = 10; // TODO: add as config envar const apiKeyHash = await bcrypt.hash(apiKey, saltRounds); - savedAPIKey = await new APIKey({ + apiKeyData = await new APIKeyData({ name, workspace, environment, @@ -55,22 +52,72 @@ router.post( iv, tag }).save(); - - // 1. generate api key - // 2. hash api key with bcrypt - // 3. store hash and api key info in db - // 4. return api key } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); return res.status(400).send({ - message: 'xxx' + message: 'Failed to create workspace API Key' }); } return res.status(200).send({ - apiKey: savedAPIKey + apiKey, + apiKeyData + }); + } +); + +// TODO: middleware +router.get( + '/', + requireAuth, + query('workspaceId').exists().trim(), + async (req, res) => { + let apiKeyData; + try { + const { workspaceId } = req.query; + + apiKeyData = await APIKeyData.find({ + workspace: workspaceId + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace API Key data' + }); + } + + return res.status(200).send({ + apiKeyData + }); + } +); + +// TODO: middleware +router.delete( + ':apiKeyDataId', + requireAuth, + // TODO: requireAPIKeyDataAuth, + param('apiKeyDataId').exists().trim(), + async (req, res) => { + let apiKeyData; + try { + const { apiKeyDataId } = req.params; + + apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to delete API key data' + }); + } + + return res.status(200).send({ + apiKeyData }); } ); From 01d969190bc24f84d2106c1db9d9bc3b22dd2dbd Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 30 Dec 2022 23:57:21 +0300 Subject: [PATCH 3/6] Begin service token data refactor --- backend/src/app.ts | 4 +- backend/src/config/index.ts | 2 + .../src/controllers/workspaceController.ts | 28 ++++ backend/src/middleware/index.ts | 2 + .../src/middleware/requireServiceTokenAuth.ts | 1 + ...Auth.ts => requireServiceTokenDataAuth.ts} | 20 +-- backend/src/models/index.ts | 6 +- backend/src/models/serviceToken.ts | 1 + .../{apiKeyData.ts => serviceTokenData .ts} | 37 +++-- backend/src/routes/apiKey.ts | 127 --------------- backend/src/routes/index.ts | 4 +- backend/src/routes/serviceTokenData.ts | 144 ++++++++++++++++++ backend/src/routes/workspace.ts | 14 +- backend/src/types/express/index.d.ts | 1 + 14 files changed, 226 insertions(+), 165 deletions(-) rename backend/src/middleware/{requireAPIKeyDataAuth.ts => requireServiceTokenDataAuth.ts} (60%) rename backend/src/models/{apiKeyData.ts => serviceTokenData .ts} (57%) delete mode 100644 backend/src/routes/apiKey.ts create mode 100644 backend/src/routes/serviceTokenData.ts diff --git a/backend/src/app.ts b/backend/src/app.ts index 500aafe77..7a263acee 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -34,7 +34,7 @@ import { stripe as stripeRouter, integration as integrationRouter, integrationAuth as integrationAuthRouter, - apiKey as apiKeyRouter + serviceTokenData as serviceTokenDataRouter } from './routes'; import { getLogger } from './utils/logger'; @@ -86,7 +86,7 @@ app.use('/api/v1/password', passwordRouter); app.use('/api/v1/stripe', stripeRouter); app.use('/api/v1/integration', integrationRouter); app.use('/api/v1/integration-auth', integrationAuthRouter); -app.use('/api/v1/api-key', apiKeyRouter); +app.use('/api/v1/service-token-data', serviceTokenDataRouter); //* Handle unrouted requests and respond with proper error message as well as status code app.use((req, res, next)=>{ diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 3fb475099..652b3c7b2 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,6 +1,7 @@ const PORT = process.env.PORT || 4000; const EMAIL_TOKEN_LIFETIME = process.env.EMAIL_TOKEN_LIFETIME! || '86400'; const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; +const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10; const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d'; const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!; const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d'; @@ -47,6 +48,7 @@ export { PORT, EMAIL_TOKEN_LIFETIME, ENCRYPTION_KEY, + SALT_ROUNDS, JWT_AUTH_LIFETIME, JWT_AUTH_SECRET, JWT_REFRESH_LIFETIME, diff --git a/backend/src/controllers/workspaceController.ts b/backend/src/controllers/workspaceController.ts index 6f3e4bd11..a843ed9d6 100644 --- a/backend/src/controllers/workspaceController.ts +++ b/backend/src/controllers/workspaceController.ts @@ -8,6 +8,7 @@ import { IntegrationAuth, IUser, ServiceToken, + ServiceTokenData, } from '../models'; import { createWorkspace as create, @@ -334,4 +335,31 @@ export const getWorkspaceServiceTokens = async ( return res.status(200).send({ serviceTokens }); +} + +export const getWorkspaceServiceTokenData = async ( + req: Request, + res: Response +) => { + let serviceTokenData; + try { + const { workspaceId } = req.query; + + serviceTokenData = await ServiceTokenData + .find({ + workspace: workspaceId + }) + .select('+encryptedKey +iv +tag'); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace service token data' + }); + } + + return res.status(200).send({ + serviceTokenData + }); } \ No newline at end of file diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index 7fcba66e1..57ddbaeb7 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -6,6 +6,7 @@ import requireOrganizationAuth from './requireOrganizationAuth'; import requireIntegrationAuth from './requireIntegrationAuth'; import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth'; import requireServiceTokenAuth from './requireServiceTokenAuth'; +import requireServiceTokenDataAuth from './requireServiceTokenDataAuth'; import validateRequest from './validateRequest'; export { @@ -17,5 +18,6 @@ export { requireIntegrationAuth, requireIntegrationAuthorizationAuth, requireServiceTokenAuth, + requireServiceTokenDataAuth, validateRequest }; diff --git a/backend/src/middleware/requireServiceTokenAuth.ts b/backend/src/middleware/requireServiceTokenAuth.ts index 94e8363ff..904f4d38e 100644 --- a/backend/src/middleware/requireServiceTokenAuth.ts +++ b/backend/src/middleware/requireServiceTokenAuth.ts @@ -4,6 +4,7 @@ import { ServiceToken } from '../models'; import { JWT_SERVICE_SECRET } from '../config'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +// TODO: deprecate declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { userId: string; diff --git a/backend/src/middleware/requireAPIKeyDataAuth.ts b/backend/src/middleware/requireServiceTokenDataAuth.ts similarity index 60% rename from backend/src/middleware/requireAPIKeyDataAuth.ts rename to backend/src/middleware/requireServiceTokenDataAuth.ts index 8dafb5a9c..1f2e33d7a 100644 --- a/backend/src/middleware/requireAPIKeyDataAuth.ts +++ b/backend/src/middleware/requireServiceTokenDataAuth.ts @@ -1,11 +1,11 @@ import { Request, Response, NextFunction } from 'express'; -import { APIKeyData } from '../models'; +import { ServiceToken, ServiceTokenData } from '../models'; import { validateMembership } from '../helpers/membership'; import { AccountNotFoundError } from '../utils/errors'; type req = 'params' | 'body' | 'query'; -const requireAPIKeyDataAuth = ({ +const requireServiceTokenDataAuth = ({ acceptedRoles, acceptedStatuses, location = 'params' @@ -16,25 +16,25 @@ const requireAPIKeyDataAuth = ({ }) => { return async (req: Request, res: Response, next: NextFunction) => { - // req.user + const serviceTokenData = await ServiceTokenData + .findById(req[location].serviceTokenDataId) + .select('+encryptedKey +iv +tag'); - const apiKeyData = await APIKeyData.findById(req[location].apiKeyDataId); - - if (!apiKeyData) { - return next(AccountNotFoundError({message: 'Failed to locate API Key data'})); + if (!serviceTokenData) { + return next(AccountNotFoundError({message: 'Failed to locate service token data'})); } await validateMembership({ userId: req.user._id.toString(), - workspaceId: apiKeyData?.workspace.toString(), + workspaceId: serviceTokenData.workspace.toString(), acceptedRoles, acceptedStatuses }); - req.apiKeyData = '' // ?? + req.serviceTokenData = serviceTokenData; next(); } } -export default requireAPIKeyDataAuth; \ No newline at end of file +export default requireServiceTokenDataAuth; \ No newline at end of file diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index fd9578523..8e934d511 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -14,7 +14,7 @@ import Token, { IToken } from './token'; import User, { IUser } from './user'; import UserAction, { IUserAction } from './userAction'; import Workspace, { IWorkspace } from './workspace'; -import APIKeyData, { IAPIKeyData } from './apiKeyData'; +import ServiceTokenData, { IServiceTokenData } from './serviceTokenData '; export { BackupPrivateKey, @@ -49,6 +49,6 @@ export { IUserAction, Workspace, IWorkspace, - APIKeyData, - IAPIKeyData, + ServiceTokenData, + IServiceTokenData }; diff --git a/backend/src/models/serviceToken.ts b/backend/src/models/serviceToken.ts index 73f705fc8..b5a2f4ec9 100644 --- a/backend/src/models/serviceToken.ts +++ b/backend/src/models/serviceToken.ts @@ -1,6 +1,7 @@ import { Schema, model, Types } from 'mongoose'; import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; +// TODO: deprecate export interface IServiceToken { _id: Types.ObjectId; name: string; diff --git a/backend/src/models/apiKeyData.ts b/backend/src/models/serviceTokenData .ts similarity index 57% rename from backend/src/models/apiKeyData.ts rename to backend/src/models/serviceTokenData .ts index 8b064bf08..01bac8d58 100644 --- a/backend/src/models/apiKeyData.ts +++ b/backend/src/models/serviceTokenData .ts @@ -1,36 +1,33 @@ import { Schema, model, Types } from 'mongoose'; import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; -export interface IAPIKeyData { +export interface IServiceTokenData { name: string; - workspaces: { - workspace: Types.ObjectId, - environments: string[] - }[]; + workspace: Types.ObjectId; + environment: string; // TODO: adapt to upcoming environment id expiresAt: Date; prefix: string; - apiKeyHash: string; + serviceTokenHash: string; encryptedKey: string; iv: string; tag: string; } -const apiKeyDataSchema = new Schema( +const serviceTokenDataSchema = new Schema( { name: { type: String, required: true }, - workspaces: [{ - workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace' - }, - environments: [{ - type: String, - enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD] - }] - }], + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + environment: { // TODO: adapt to upcoming environment id + type: String, + required: true + }, expiresAt: { type: Date }, @@ -38,7 +35,7 @@ const apiKeyDataSchema = new Schema( type: String, required: true }, - apiKeyHash: { + serviceTokenHash: { type: String, unique: true, required: true @@ -61,6 +58,6 @@ const apiKeyDataSchema = new Schema( } ); -const APIKeyData = model('APIKeyData', apiKeyDataSchema); +const ServiceTokenData = model('ServiceTokenData', serviceTokenDataSchema); -export default APIKeyData; +export default ServiceTokenData; diff --git a/backend/src/routes/apiKey.ts b/backend/src/routes/apiKey.ts deleted file mode 100644 index a40c852ac..000000000 --- a/backend/src/routes/apiKey.ts +++ /dev/null @@ -1,127 +0,0 @@ -import express from 'express'; -const router = express.Router(); -import { - requireAuth -} from '../middleware'; -import { - APIKeyData -} from '../models'; -import { param, body, query } from 'express-validator'; -import crypto from 'crypto'; -import bcrypt from 'bcrypt'; -import * as Sentry from '@sentry/node'; - -// TODO: middleware -router.post( - '/', - requireAuth, - body('name').exists().trim(), - body('workspace'), - body('environment'), - body('encryptedKey'), - body('iv'), - body('tag'), - body('expiresAt'), - async (req, res) => { - let apiKey, apiKeyData; - try { - const { - name, - workspace, - environment, - encryptedKey, - iv, - tag, - expiresAt - } = req.body; - - // create 38-char API key with first 6-char being the prefix - apiKey = crypto.randomBytes(19).toString('hex'); - - const saltRounds = 10; // TODO: add as config envar - const apiKeyHash = await bcrypt.hash(apiKey, saltRounds); - - apiKeyData = await new APIKeyData({ - name, - workspace, - environment, - expiresAt, - prefix: apiKey.substring(0, 6), - apiKeyHash, - encryptedKey, - iv, - tag - }).save(); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to create workspace API Key' - }); - } - - return res.status(200).send({ - apiKey, - apiKeyData - }); - } -); - -// TODO: middleware -router.get( - '/', - requireAuth, - query('workspaceId').exists().trim(), - async (req, res) => { - let apiKeyData; - try { - const { workspaceId } = req.query; - - apiKeyData = await APIKeyData.find({ - workspace: workspaceId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace API Key data' - }); - } - - return res.status(200).send({ - apiKeyData - }); - } -); - -// TODO: middleware -router.delete( - ':apiKeyDataId', - requireAuth, - // TODO: requireAPIKeyDataAuth, - param('apiKeyDataId').exists().trim(), - async (req, res) => { - let apiKeyData; - try { - const { apiKeyDataId } = req.params; - - apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete API key data' - }); - } - - return res.status(200).send({ - apiKeyData - }); - } -); - -// INFISICAL TOKEN = . - -export default router; \ No newline at end of file diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index bc0ff776d..89b02ebc9 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -15,7 +15,7 @@ import password from './password'; import stripe from './stripe'; import integration from './integration'; import integrationAuth from './integrationAuth'; -import apiKey from './apiKey'; +import serviceTokenData from './serviceTokenData'; export { signup, @@ -35,5 +35,5 @@ export { stripe, integration, integrationAuth, - apiKey + serviceTokenData }; diff --git a/backend/src/routes/serviceTokenData.ts b/backend/src/routes/serviceTokenData.ts new file mode 100644 index 000000000..903e72771 --- /dev/null +++ b/backend/src/routes/serviceTokenData.ts @@ -0,0 +1,144 @@ +import express from 'express'; +const router = express.Router(); +import crypto from 'crypto'; +import bcrypt from 'bcrypt'; +import * as Sentry from '@sentry/node'; +import { + requireAuth, + requireWorkspaceAuth, + requireServiceTokenDataAuth, + validateRequest +} from '../middleware'; +import { + ServiceTokenData +} from '../models'; +import { param, body, query } from 'express-validator'; +import { + SALT_ROUNDS +} from '../config'; +import { + ADMIN, + MEMBER, + COMPLETED, + GRANTED +} from '../variables'; + +// TODO: move logic into separate controller (probably after pull with latest routing) + +/** + * 2 different concepts that we should distinguish between: + * - API key (user) - allows user to perform queries and mutations on whatever + * their account could access (better than JWT because it has ACL and scoping). + * - Service token (bound to a workspace and environment). + */ + +/** + * Service token flow? + * 1. Post service token data details including project key encrypted under on cient-side. + * 2. Construct on client-side as =. + * 3. Need for CLI to be able to get back service token details + */ + +router.post( + '/', + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED], + location: 'body' + }), + requireAuth, + body('name').exists().trim(), + body('workspace'), + body('environment'), + body('encryptedKey'), + body('iv'), + body('tag'), + body('expiresAt'), + validateRequest, + async (req, res) => { + let serviceToken, serviceTokenData; + try { + const { + name, + workspace, + environment, + encryptedKey, + iv, + tag, + expiresAt + } = req.body; + + // create 38-char service token with first 6-char being the prefix + serviceToken = crypto.randomBytes(19).toString('hex'); + + const serviceTokenHash = await bcrypt.hash(serviceToken, SALT_ROUNDS); + + serviceTokenData = await new ServiceTokenData({ + name, + workspace, + environment, + expiresAt, + prefix: serviceToken.substring(0, 6), + serviceTokenHash, + encryptedKey, + iv, + tag + }).save(); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to create service token data' + }); + } + + return res.status(200).send({ + serviceToken, + serviceTokenData + }); + } +); + +// TODO: CLI has to get service token details without needing a JWT +router.get( + '/:serviceTokenDataId', + requireAuth, + requireServiceTokenDataAuth, + param('serviceTokenDataId').exists().trim(), + validateRequest, + async (req, res) => { + return ({ + serviceTokenData: req.serviceTokenData + }); + } +); + +router.delete( + '/:serviceTokenDataId', + requireAuth, + requireServiceTokenDataAuth, + param('serviceTokenDataId').exists().trim(), + validateRequest, + async (req, res) => { + let serviceTokenData; + try { + const { serviceTokenDataId } = req.params; + + serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to delete service token data' + }); + } + + return res.status(200).send({ + serviceTokenData + }); + } +); + +export default router; \ No newline at end of file diff --git a/backend/src/routes/workspace.ts b/backend/src/routes/workspace.ts index acd2aaf8b..3551841c7 100644 --- a/backend/src/routes/workspace.ts +++ b/backend/src/routes/workspace.ts @@ -119,7 +119,7 @@ router.get( ); router.get( - '/:workspaceId/service-tokens', + '/:workspaceId/service-tokens', // deprecate requireAuth, requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -130,4 +130,16 @@ router.get( workspaceController.getWorkspaceServiceTokens ); +router.get( + '/:workspaceId/service-token-data', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspaceServiceTokenData +); + export default router; diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index 319562fd0..a019be1e9 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -15,6 +15,7 @@ declare global { bot: any; serviceToken: any; accessToken: any; + serviceTokenData: any; query?: any; } } From b8a64714d25970acde15bafe833f6185bbed6c02 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 1 Jan 2023 09:24:20 +0700 Subject: [PATCH 4/6] Refactor auth middleware to accept multiple auth modes --- .../v1/serviceTokenDataController.ts | 10 +- .../src/controllers/v2/workspaceController.ts | 2 +- backend/src/helpers/auth.ts | 130 +++++++++++++----- backend/src/middleware/requireAuth.ts | 37 +++-- backend/src/models/serviceTokenData .ts | 8 +- backend/src/routes/v1/serviceTokenData.ts | 2 +- backend/src/routes/v2/workspace.ts | 6 +- backend/src/utils/errors.ts | 10 ++ 8 files changed, 138 insertions(+), 67 deletions(-) diff --git a/backend/src/controllers/v1/serviceTokenDataController.ts b/backend/src/controllers/v1/serviceTokenDataController.ts index 83701abcd..3a4b94a83 100644 --- a/backend/src/controllers/v1/serviceTokenDataController.ts +++ b/backend/src/controllers/v1/serviceTokenDataController.ts @@ -48,7 +48,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => { const expiresAt = new Date(); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); - serviceTokenData = await new ServiceTokenData({ + // create service token data + serviceTokenData = new ServiceTokenData({ name, workspace: workspaceId, environment, @@ -59,7 +60,12 @@ export const createServiceTokenData = async (req: Request, res: Response) => { encryptedKey, iv, tag - }).save(); + }) + + await serviceTokenData.save(); + + // return service token data without sensitive data + serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); } catch (err) { Sentry.setUser({ email: req.user.email }); diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index fa0ffa5af..1b10ebccd 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -139,7 +139,7 @@ export const pullSecrets = async (req: Request, res: Response) => { environment }); - if (channel !== 'cli') { // TODO: fix frontend to get rid of this reformat bs + if (channel !== 'cli') { secrets = reformatPullSecrets({ secrets }); } diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 9845e9c74..c3b212916 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -12,56 +12,112 @@ import { JWT_REFRESH_SECRET, SALT_ROUNDS } from '../config'; +import { + AccountNotFoundError, + ServiceTokenDataNotFoundError, + UnauthorizedRequestError +} from '../utils/errors'; /** - * Attach auth payload + * Validate that auth token value [authTokenValue] falls under one of + * accepted auth modes [acceptedAuthModes]. * @param {Object} obj - * @param {String} obj.authTokenValue + * @param {String} obj.authTokenValue - auth token value (e.g. JWT or service token value) + * @param {String[]} obj.acceptedAuthModes - accepted auth modes (e.g. jwt, serviceToken) + * @returns {String} authMode - auth mode */ -const attachAuthPayload = async ({ +const validateAuthMode = ({ + authTokenValue, + acceptedAuthModes +}: { + authTokenValue: string; + acceptedAuthModes: string[]; +}) => { + let authMode; + try { + switch (authTokenValue.split('.', 1)[0]) { + case 'st': + authMode = 'serviceToken'; + break; + default: + authMode = 'jwt'; + break; + } + + if (!acceptedAuthModes.includes(authMode)) + throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' }); + + } catch (err) { + throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' }); + } + + return authMode; +} + +/** + * Return user payload corresponding to JWT token [authTokenValue] + * @param {Object} obj + * @param {String} obj.authTokenValue - JWT token value + * @returns {User} user - user corresponding to JWT token + */ +const getAuthUserPayload = async ({ authTokenValue }: { authTokenValue: string; }) => { - let serviceTokenHash, decodedToken; // intermediate variables - let serviceTokenData, user; // payloads + let user; try { - switch (authTokenValue.split('.', 1)[0]) { - case 'st': - // case: service token auth mode - serviceTokenHash = await bcrypt.hash(authTokenValue, SALT_ROUNDS); - serviceTokenData = await ServiceTokenData - .findOne({ - serviceTokenHash - }) - .select('+encryptedKey +iv +tag'); - - if (!serviceTokenData) { - throw new Error('Account not found error'); - } + const decodedToken = ( + jwt.verify(authTokenValue, JWT_AUTH_SECRET) + ); - return serviceTokenData; - default: - // case: JWT token auth mode - decodedToken = ( - jwt.verify(authTokenValue, JWT_AUTH_SECRET) - ); - - user = await User.findOne({ - _id: decodedToken.userId - }).select('+publicKey'); + user = await User.findOne({ + _id: decodedToken.userId + }).select('+publicKey'); - if (!user) - throw new Error('Account not found error'); + if (!user) throw AccountNotFoundError({ message: 'Failed to find User' }); - if (!user?.publicKey) - throw new Error('Unable to authenticate due to partially set up account'); + if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' }); - return user; - } } catch (err) { - throw new Error('Failed to attach auth payload'); + throw UnauthorizedRequestError({ + message: 'Failed to authenticate JWT token' + }); } + + return user; +} + +/** + * Return service token data payload corresponding to service token [authTokenValue] + * @param {Object} obj + * @param {String} obj.authTokenValue - service token value + * @returns {ServiceTokenData} serviceTokenData - service token data + */ +const getAuthSTDPayload = async ({ + authTokenValue +}: { + authTokenValue: string; +}) => { + let serviceTokenData; + try { + const serviceTokenHash = await bcrypt.hash(authTokenValue, SALT_ROUNDS); + + serviceTokenData = await ServiceTokenData + .findOne({ + serviceTokenHash + }) + .select('+encryptedKey +iv +tag'); + + if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); + + } catch (err) { + throw UnauthorizedRequestError({ + message: 'Failed to authenticate service token' + }); + } + + return serviceTokenData; } /** @@ -154,7 +210,9 @@ const createToken = ({ }; export { - attachAuthPayload, + validateAuthMode, + getAuthUserPayload, + getAuthSTDPayload, createToken, issueTokens, clearTokens diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index 52f69d260..b91d2cd09 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -2,7 +2,9 @@ import jwt from 'jsonwebtoken'; import { Request, Response, NextFunction } from 'express'; import { User, ServiceTokenData } from '../models'; import { - attachAuthPayload + validateAuthMode, + getAuthUserPayload, + getAuthSTDPayload } from '../helpers/auth'; import { JWT_AUTH_SECRET } from '../config'; import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors'; @@ -37,30 +39,25 @@ const requireAuth = ({ if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) - // validate auth mode - let authMode; - switch (AUTH_TOKEN_VALUE.split('.', 1)[0]) { - case 'st': - authMode = 'st'; - break; - default: - authMode = 'jwt'; - break; - } - - if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode'); - - // attach auth request payload - const payload = await attachAuthPayload({ - authTokenValue: AUTH_TOKEN_VALUE + // validate auth token against + const authMode = validateAuthMode({ + authTokenValue: AUTH_TOKEN_VALUE, + acceptedAuthModes }); + if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode'); + + // attach auth payloads switch (authMode) { - case 'st': - req.serviceTokenData = payload; + case 'serviceToken': + req.serviceTokenData = await getAuthSTDPayload({ + authTokenValue: AUTH_TOKEN_VALUE + }); break; default: - req.user = payload; + req.user = await getAuthUserPayload({ + authTokenValue: AUTH_TOKEN_VALUE + }); break; } diff --git a/backend/src/models/serviceTokenData .ts b/backend/src/models/serviceTokenData .ts index 32520dda9..1faecfaff 100644 --- a/backend/src/models/serviceTokenData .ts +++ b/backend/src/models/serviceTokenData .ts @@ -45,19 +45,19 @@ const serviceTokenDataSchema = new Schema( type: String, unique: true, required: true, - select: true + select: false }, encryptedKey: { type: String, - select: true + select: false }, iv: { type: String, - select: true + select: false }, tag: { type: String, - select: true + select: false } }, { diff --git a/backend/src/routes/v1/serviceTokenData.ts b/backend/src/routes/v1/serviceTokenData.ts index fa9f4fdbd..4223172d3 100644 --- a/backend/src/routes/v1/serviceTokenData.ts +++ b/backend/src/routes/v1/serviceTokenData.ts @@ -18,7 +18,7 @@ import { serviceTokenDataController } from '../../controllers/v1'; router.get( '/', requireAuth({ - acceptedAuthModes: ['st'] + acceptedAuthModes: ['serviceToken'] }), param('serviceTokenDataId').exists().trim(), validateRequest, diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index 6fc81bbc5..df26749f3 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -11,7 +11,7 @@ import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; import { membershipController } from '../../controllers/v1'; import { workspaceController } from '../../controllers/v2'; -router.post( // unfinished +router.post( '/:workspaceId/secrets', requireAuth({ acceptedAuthModes: ['jwt'] @@ -29,10 +29,10 @@ router.post( // unfinished workspaceController.pushWorkspaceSecrets ); -router.get( // unfinished, check that it works with st +router.get( '/:workspaceId/secrets', requireAuth({ - acceptedAuthModes: ['jwt', 'st'] + acceptedAuthModes: ['jwt', 'serviceToken'] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts index 40c467131..2b2ce59a6 100644 --- a/backend/src/utils/errors.ts +++ b/backend/src/utils/errors.ts @@ -113,4 +113,14 @@ export const AccountNotFoundError = (error?: Partial) => ne stack: error?.stack }) +//* ----->[SERVICE TOKEN DATA ERRORS]<----- +export const ServiceTokenDataNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'service_token_data_not_found_error', + message: error?.message ?? 'The requested service token data was not found', + context: error?.context, + stack: error?.stack +}) + //* ----->[MISC ERRORS]<----- From e99ee94a7b5bfcb27c9eae9a8466bfb270449f46 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 2 Jan 2023 22:43:00 +0700 Subject: [PATCH 5/6] Modify service token format --- .../v1/serviceTokenDataController.ts | 25 +++++++--------- backend/src/helpers/auth.ts | 30 ++++++++++++++----- backend/src/middleware/requireAuth.ts | 3 +- .../src/middleware/requireWorkspaceAuth.ts | 6 ++-- backend/src/models/serviceTokenData .ts | 9 ++---- backend/src/routes/v1/serviceTokenData.ts | 4 +-- backend/src/routes/v2/workspace.ts | 2 -- 7 files changed, 41 insertions(+), 38 deletions(-) diff --git a/backend/src/controllers/v1/serviceTokenDataController.ts b/backend/src/controllers/v1/serviceTokenDataController.ts index 3a4b94a83..d8f4d4eea 100644 --- a/backend/src/controllers/v1/serviceTokenDataController.ts +++ b/backend/src/controllers/v1/serviceTokenDataController.ts @@ -15,7 +15,7 @@ import { * @param res * @returns */ -export const getServiceTokenData = async (req: Request, res: Response) => ({ +export const getServiceTokenData = async (req: Request, res: Response) => res.status(200).send({ serviceTokenData: req.serviceTokenData }); @@ -38,35 +38,32 @@ export const createServiceTokenData = async (req: Request, res: Response) => { tag, expiresIn } = req.body; + + const secret = crypto.randomBytes(16).toString('hex'); + const secretHash = await bcrypt.hash(secret, SALT_ROUNDS); - // create 41-char service token with first 9-char being the prefix - serviceToken = `st.${crypto.randomBytes(19).toString('hex')}`; - - const serviceTokenHash = await bcrypt.hash(serviceToken, SALT_ROUNDS); - - // compute access token expiration date const expiresAt = new Date(); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); - // create service token data - serviceTokenData = new ServiceTokenData({ + serviceTokenData = await new ServiceTokenData({ name, workspace: workspaceId, environment, user: req.user._id, expiresAt, - prefix: serviceToken.substring(0, 9), - serviceTokenHash, + secretHash, encryptedKey, iv, tag - }) + }).save(); - await serviceTokenData.save(); - // return service token data without sensitive data serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); + if (!serviceTokenData) throw new Error('Failed to find service token data'); + + serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; + } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index c3b212916..ad63d41b4 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -15,7 +15,8 @@ import { import { AccountNotFoundError, ServiceTokenDataNotFoundError, - UnauthorizedRequestError + UnauthorizedRequestError, + BadRequestError } from '../utils/errors'; /** @@ -101,15 +102,30 @@ const getAuthSTDPayload = async ({ }) => { let serviceTokenData; try { - const serviceTokenHash = await bcrypt.hash(authTokenValue, SALT_ROUNDS); + const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3); + + // TODO: optimize double query + serviceTokenData = await ServiceTokenData + .findById(TOKEN_IDENTIFIER, 'secretHash expiresAt'); + + if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) { + // case: service token expired + await ServiceTokenData.findByIdAndDelete(serviceTokenData._id); + throw UnauthorizedRequestError({ + message: 'Failed to authenticate expired service token' + }); + } + + if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); + + const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash); + if (!isMatch) throw UnauthorizedRequestError({ + message: 'Failed to authenticate service token' + }); serviceTokenData = await ServiceTokenData - .findOne({ - serviceTokenHash - }) + .findById(TOKEN_IDENTIFIER) .select('+encryptedKey +iv +tag'); - - if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); } catch (err) { throw UnauthorizedRequestError({ diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index b91d2cd09..8253cb64e 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -6,8 +6,7 @@ import { getAuthUserPayload, getAuthSTDPayload } from '../helpers/auth'; -import { JWT_AUTH_SECRET } from '../config'; -import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +import { BadRequestError } from '../utils/errors'; declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index 2a9110c2c..68edec6a4 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -40,10 +40,10 @@ const requireWorkspaceAuth = ({ if ( req.serviceTokenData && req.serviceTokenData.workspace !== workspaceId - && req.serviceTokenData.environment !== req.body.environment - ) - // case: st auth + && req.serviceTokenData.environment !== req.query.environment + ) { next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'})) + } return next(); } catch (err) { diff --git a/backend/src/models/serviceTokenData .ts b/backend/src/models/serviceTokenData .ts index 1faecfaff..8e8ae5eab 100644 --- a/backend/src/models/serviceTokenData .ts +++ b/backend/src/models/serviceTokenData .ts @@ -7,8 +7,7 @@ export interface IServiceTokenData { environment: string; // TODO: adapt to upcoming environment id user: Types.ObjectId; expiresAt: Date; - prefix: string; - serviceTokenHash: string; + secretHash: string; encryptedKey: string; iv: string; tag: string; @@ -37,11 +36,7 @@ const serviceTokenDataSchema = new Schema( expiresAt: { type: Date }, - prefix: { - type: String, - required: true - }, - serviceTokenHash: { + secretHash: { type: String, unique: true, required: true, diff --git a/backend/src/routes/v1/serviceTokenData.ts b/backend/src/routes/v1/serviceTokenData.ts index 4223172d3..1d7700615 100644 --- a/backend/src/routes/v1/serviceTokenData.ts +++ b/backend/src/routes/v1/serviceTokenData.ts @@ -1,4 +1,4 @@ -import express, { Request, Response } from 'express'; +import express from 'express'; const router = express.Router(); import { requireAuth, @@ -20,8 +20,6 @@ router.get( requireAuth({ acceptedAuthModes: ['serviceToken'] }), - param('serviceTokenDataId').exists().trim(), - validateRequest, serviceTokenDataController.getServiceTokenData ); diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index df26749f3..b2c91bd1b 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -4,11 +4,9 @@ import { body, param, query } from 'express-validator'; import { requireAuth, requireWorkspaceAuth, - requireServiceTokenAuth, validateRequest } from '../../middleware'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; -import { membershipController } from '../../controllers/v1'; import { workspaceController } from '../../controllers/v2'; router.post( From e9601307efdbea93f8fdae15a69e4b800d1859c3 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 3 Jan 2023 09:33:00 +0700 Subject: [PATCH 6/6] Move service token data routes and controllers to v2 --- backend/src/app.ts | 6 +++--- backend/src/controllers/v1/index.ts | 4 +--- backend/src/controllers/v2/index.ts | 4 +++- .../controllers/{v1 => v2}/serviceTokenDataController.ts | 0 backend/src/ee/routes/v1/workspace.ts | 1 - backend/src/routes/v1/index.ts | 4 +--- backend/src/routes/v2/index.ts | 4 +++- backend/src/routes/{v1 => v2}/serviceTokenData.ts | 2 +- 8 files changed, 12 insertions(+), 13 deletions(-) rename backend/src/controllers/{v1 => v2}/serviceTokenDataController.ts (100%) rename backend/src/routes/{v1 => v2}/serviceTokenData.ts (95%) diff --git a/backend/src/app.ts b/backend/src/app.ts index f37fd7560..ca6428447 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -29,7 +29,6 @@ import { userAction as v1UserActionRouter, secret as v1SecretRouter, serviceToken as v1ServiceTokenRouter, - serviceTokenData as v1ServiceTokenDataRouter, password as v1PasswordRouter, stripe as v1StripeRouter, integration as v1IntegrationRouter, @@ -37,7 +36,8 @@ import { } from './routes/v1'; import { secret as v2SecretRouter, - workspace as v2WorkspaceRouter + workspace as v2WorkspaceRouter, + serviceTokenData as v2ServiceTokenDataRouter, } from './routes/v2'; import { getLogger } from './utils/logger'; @@ -85,7 +85,6 @@ app.use('/api/v1/key', v1KeyRouter); app.use('/api/v1/invite-org', v1InviteOrgRouter); app.use('/api/v1/secret', v1SecretRouter); app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate -app.use('/api/v1/service-token-data', v1ServiceTokenDataRouter); app.use('/api/v1/password', v1PasswordRouter); app.use('/api/v1/stripe', v1StripeRouter); app.use('/api/v1/integration', v1IntegrationRouter); @@ -94,6 +93,7 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); // v2 routes app.use('/api/v2/workspace', v2WorkspaceRouter); app.use('/api/v2/secret', v2SecretRouter); +app.use('/api/v2/service-token-data', v2ServiceTokenDataRouter); //* Handle unrouted requests and respond with proper error message as well as status code app.use((req, res, next)=>{ diff --git a/backend/src/controllers/v1/index.ts b/backend/src/controllers/v1/index.ts index 56c6071c5..1da61835f 100644 --- a/backend/src/controllers/v1/index.ts +++ b/backend/src/controllers/v1/index.ts @@ -14,7 +14,6 @@ import * as stripeController from './stripeController'; import * as userActionController from './userActionController'; import * as userController from './userController'; import * as workspaceController from './workspaceController'; -import * as serviceTokenDataController from './serviceTokenDataController'; export { authController, @@ -32,6 +31,5 @@ export { stripeController, userActionController, userController, - workspaceController, - serviceTokenDataController + workspaceController }; diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts index dc6977c91..d4729c15c 100644 --- a/backend/src/controllers/v2/index.ts +++ b/backend/src/controllers/v2/index.ts @@ -1,5 +1,7 @@ import * as workspaceController from './workspaceController'; +import * as serviceTokenDataController from './serviceTokenDataController'; export { - workspaceController + workspaceController, + serviceTokenDataController } diff --git a/backend/src/controllers/v1/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts similarity index 100% rename from backend/src/controllers/v1/serviceTokenDataController.ts rename to backend/src/controllers/v2/serviceTokenDataController.ts diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index d127599e1..bc3480280 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -25,5 +25,4 @@ router.get( workspaceController.getWorkspaceSecretSnapshots ); - export default router; \ No newline at end of file diff --git a/backend/src/routes/v1/index.ts b/backend/src/routes/v1/index.ts index 89b02ebc9..2dfe58baa 100644 --- a/backend/src/routes/v1/index.ts +++ b/backend/src/routes/v1/index.ts @@ -15,7 +15,6 @@ import password from './password'; import stripe from './stripe'; import integration from './integration'; import integrationAuth from './integrationAuth'; -import serviceTokenData from './serviceTokenData'; export { signup, @@ -34,6 +33,5 @@ export { password, stripe, integration, - integrationAuth, - serviceTokenData + integrationAuth }; diff --git a/backend/src/routes/v2/index.ts b/backend/src/routes/v2/index.ts index 6e6758753..acf115a92 100644 --- a/backend/src/routes/v2/index.ts +++ b/backend/src/routes/v2/index.ts @@ -1,7 +1,9 @@ import secret from './secret'; import workspace from './workspace'; +import serviceTokenData from './serviceTokenData'; export { secret, - workspace + workspace, + serviceTokenData } diff --git a/backend/src/routes/v1/serviceTokenData.ts b/backend/src/routes/v2/serviceTokenData.ts similarity index 95% rename from backend/src/routes/v1/serviceTokenData.ts rename to backend/src/routes/v2/serviceTokenData.ts index 1d7700615..578d4e38e 100644 --- a/backend/src/routes/v1/serviceTokenData.ts +++ b/backend/src/routes/v2/serviceTokenData.ts @@ -13,7 +13,7 @@ import { COMPLETED, GRANTED } from '../../variables'; -import { serviceTokenDataController } from '../../controllers/v1'; +import { serviceTokenDataController } from '../../controllers/v2'; router.get( '/',