From 864f77a28d06247b5d53a5a6ee4a99eaf8d849f3 Mon Sep 17 00:00:00 2001 From: vterzic Date: Tue, 9 Aug 2022 10:05:06 +0200 Subject: [PATCH] V6/refactor/auth module (#1986) * Add authentication database migrations * Add token-generation script * Rename permission table to role_ability * Make token id a string * Add jwt-util * Split jwt decoding and getPayload functions * Decouple test from jsonwebtoken lib * Update doc * Add test for expired jwt validation * Implement isAuthenticated method * Implement isAuthorized method * Implement abilities fetching * Implement isActionPublic method * rename isPublicAction method to isActionPublic * Implement auth middlewares * Update config structure * Fix isRevoked promise bug * Fix token-generation script * Integrate auth middlewares * Fix repository to accept tokenId param * Remove unused middleware * Move middlewares to separate folder * Fix method logic to fit method name * Add tests for middlewares * Update config.json * Use publicOperation instead of publicAction * Merge _isRevoked method inside of _isTokenValid method * Fix typo * Fix model association bug * Add logging for auth-service * Fix bug when token is valid but not recorded in the database * Update config.json with loggingEnabled field * Enable pre-commit git hook --- .eslintrc.js | 8 + .husky/pre-commit | 2 +- config/config.json | 44 +- package-lock.json | 476 +++++++++++++++++- package.json | 4 + src/controller/http-api-router.js | 10 + .../http-client/http-client-module-manager.js | 17 + .../implementation/express-http-client.js | 46 +- .../middleware/authentication-middleware.js | 37 ++ .../middleware/authorization-middleware.js | 22 + .../rate-limiter-middleware.js | 0 .../request-validation-middleware.js | 0 .../20220624103229-create-ability.js | 29 ++ .../migrations/20220624103610-create-role.js | 29 ++ .../migrations/20220624103615-create-user.js | 36 ++ .../migrations/20220624103658-create-token.js | 44 ++ .../20220624113659-create-role-ability.js | 39 ++ ...0628113824-add-predefined-auth-entities.js | 78 +++ .../sequelize/models/ability.js | 33 ++ .../sequelize/models/role-ability.js | 34 ++ .../implementation/sequelize/models/role.js | 37 ++ .../implementation/sequelize/models/token.js | 45 ++ .../implementation/sequelize/models/user.js | 37 ++ .../sequelize/sequelize-repository.js | 37 ++ .../repository/repository-module-manager.js | 24 + src/service/auth-service.js | 145 ++++++ src/service/util/jwt-util.js | 67 +++ .../authentication-middleware.test.js | 85 ++++ .../authorization-middleware.test.js | 85 ++++ test/unit/service/auth-service.test.js | 321 ++++++++++++ test/unit/service/util/jwt-util.test.js | 108 ++++ tools/token-generation.js | 174 +++++++ 32 files changed, 2107 insertions(+), 46 deletions(-) create mode 100644 src/modules/http-client/implementation/middleware/authentication-middleware.js create mode 100644 src/modules/http-client/implementation/middleware/authorization-middleware.js rename src/modules/http-client/implementation/{ => middleware}/rate-limiter-middleware.js (100%) rename src/modules/http-client/implementation/{ => middleware}/request-validation-middleware.js (100%) create mode 100644 src/modules/repository/implementation/sequelize/migrations/20220624103229-create-ability.js create mode 100644 src/modules/repository/implementation/sequelize/migrations/20220624103610-create-role.js create mode 100644 src/modules/repository/implementation/sequelize/migrations/20220624103615-create-user.js create mode 100644 src/modules/repository/implementation/sequelize/migrations/20220624103658-create-token.js create mode 100644 src/modules/repository/implementation/sequelize/migrations/20220624113659-create-role-ability.js create mode 100644 src/modules/repository/implementation/sequelize/migrations/20220628113824-add-predefined-auth-entities.js create mode 100644 src/modules/repository/implementation/sequelize/models/ability.js create mode 100644 src/modules/repository/implementation/sequelize/models/role-ability.js create mode 100644 src/modules/repository/implementation/sequelize/models/role.js create mode 100644 src/modules/repository/implementation/sequelize/models/token.js create mode 100644 src/modules/repository/implementation/sequelize/models/user.js create mode 100644 src/service/auth-service.js create mode 100644 src/service/util/jwt-util.js create mode 100644 test/unit/middleware/authentication-middleware.test.js create mode 100644 test/unit/middleware/authorization-middleware.test.js create mode 100644 test/unit/service/auth-service.test.js create mode 100644 test/unit/service/util/jwt-util.test.js create mode 100644 tools/token-generation.js diff --git a/.eslintrc.js b/.eslintrc.js index 5b8e83d918..c3c2fae8c0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,4 +19,12 @@ module.exports = { 'no-continue': 1, 'no-underscore-dangle': 0, }, + "overrides": [ + { + "files": ["*.test.js", "*.spec.js"], + "rules": { + "no-unused-expressions": "off" + } + } + ] }; diff --git a/.husky/pre-commit b/.husky/pre-commit index eb0b000614..57757f4eda 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -#npm run lint-staged +npm run lint-staged diff --git a/config/config.json b/config/config.json index 470914c0e7..e8d64562a7 100644 --- a/config/config.json +++ b/config/config.json @@ -152,7 +152,6 @@ "port": 9000, "bootstrap": [] }, - "ipWhitelist": ["::1", "127.0.0.1"], "telemetryHub": { "enabled": false, "packages": ["ot-telemetry-collector"], @@ -160,6 +159,16 @@ }, "operationalDatabase": { "databaseName": "operationaldb" + }, + "auth": { + "ipBasedAuthEnabled": true, + "tokenBasedAuthEnabled": false, + "loggingEnabled": true, + "ipWhitelist": [ + "::1", + "127.0.0.1" + ], + "publicOperations": [] } }, "test": { @@ -267,7 +276,6 @@ "port": 9000, "bootstrap": [] }, - "ipWhitelist": ["::1", "127.0.0.1"], "telemetryHub": { "enabled": false, "packages": ["ot-telemetry-collector"], @@ -275,6 +283,16 @@ }, "operationalDatabase": { "databaseName": "operationaldb" + }, + "auth": { + "ipBasedAuthEnabled": true, + "tokenBasedAuthEnabled": false, + "loggingEnabled": true, + "ipWhitelist": [ + "::1", + "127.0.0.1" + ], + "publicOperations": [] } }, "testnet": { @@ -432,7 +450,6 @@ "/ip4/46.101.153.21/tcp/9000/p2p/QmXzmTqVT3TPUtTz4dBDN5NWSABqnX9rKXCG9WCLXMfEaM" ] }, - "ipWhitelist": ["::1", "127.0.0.1"], "telemetryHub": { "enabled": true, "packages": ["ot-telemetry-collector"], @@ -440,6 +457,16 @@ }, "operationalDatabase": { "databaseName": "operationaldb" + }, + "auth": { + "ipBasedAuthEnabled": true, + "tokenBasedAuthEnabled": false, + "loggingEnabled": true, + "ipWhitelist": [ + "::1", + "127.0.0.1" + ], + "publicOperations": [] } }, "mainnet": { @@ -552,7 +579,6 @@ "port": 9000, "bootstrap": [] }, - "ipWhitelist": ["::1", "127.0.0.1"], "telemetryHub": { "enabled": false, "packages": ["ot-telemetry-collector"], @@ -560,6 +586,16 @@ }, "operationalDatabase": { "databaseName": "operationaldb" + }, + "auth": { + "ipBasedAuthEnabled": true, + "tokenBasedAuthEnabled": false, + "loggingEnabled": true, + "ipWhitelist": [ + "::1", + "127.0.0.1" + ], + "publicOperations": [] } } } diff --git a/package-lock.json b/package-lock.json index 0e17d3b37d..f94c61e35e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "fastq": "^1.13.0", "fs-extra": "^10.0.0", "graphdb": "^2.0.0", + "ip": "^1.1.8", "it-concat": "^2.0.0", "it-length-prefixed": "^5.0.3", "it-map": "^1.0.6", @@ -42,6 +43,7 @@ "json-stable-stringify": "^1.0.1", "jsonld": "^5.2.0", "jsonschema": "^1.4.1", + "jsonwebtoken": "^8.5.1", "keccak256": "^1.0.6", "libp2p": "^0.32.4", "libp2p-bootstrap": "^0.13.0", @@ -52,6 +54,7 @@ "libp2p-tcp": "^0.17.2", "merkle-tools": "^1.4.1", "merkletreejs": "^0.2.32", + "ms": "^2.1.3", "multiformats": "^9.4.7", "mysql2": "^2.3.3", "n3": "^1.12.2", @@ -101,6 +104,7 @@ "openzeppelin-solidity": "3.4.2", "prettier": "^2.6.2", "request": "^2.88.2", + "sinon": "^14.0.0", "slugify": "^1.6.5", "solc": "0.7.6" }, @@ -3515,6 +3519,41 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.1.tgz", + "integrity": "sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "node_modules/@stablelib/aead": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/aead/-/aead-1.0.1.tgz", @@ -5291,6 +5330,11 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6540,6 +6584,11 @@ } } }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -6976,6 +7025,14 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/editorconfig": { "version": "0.15.3", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", @@ -10096,9 +10153,9 @@ } }, "node_modules/ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", + "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" }, "node_modules/ip-address": { "version": "8.1.0", @@ -11261,6 +11318,35 @@ "node": "*" } }, + "node_modules/jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=4", + "npm": ">=1.4.28" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/jsprim": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", @@ -11289,6 +11375,31 @@ "node": ">=4.0" } }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/k-bucket": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/k-bucket/-/k-bucket-5.1.0.tgz", @@ -11983,11 +12094,52 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -12783,12 +12935,6 @@ "node": ">=10" } }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/mocha/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -12967,9 +13113,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/multiaddr": { "version": "10.0.1", @@ -13366,6 +13512,34 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, + "node_modules/nise": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz", + "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": ">=5", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -16384,6 +16558,54 @@ "semver": "bin/semver.js" } }, + "node_modules/sinon": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.0.tgz", + "integrity": "sha512-ugA6BFmE+WrJdh0owRZHToLd32Uw3Lxq6E6LtNRU+xTVBefx632h03Q7apXWRsRdZAJ41LB8aUfn2+O4jsDNMw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": "^9.1.2", + "@sinonjs/samsam": "^6.1.1", + "diff": "^5.0.0", + "nise": "^5.1.1", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/slice-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", @@ -23607,6 +23829,41 @@ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==" }, + "@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/samsam": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.1.tgz", + "integrity": "sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@stablelib/aead": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/aead/-/aead-1.0.1.tgz", @@ -25107,6 +25364,11 @@ "ieee754": "^1.2.1" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -26099,6 +26361,13 @@ "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } } }, "decamelize": { @@ -26452,6 +26721,14 @@ "safer-buffer": "^2.1.0" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "editorconfig": { "version": "0.15.3", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", @@ -28998,9 +29275,9 @@ } }, "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", + "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" }, "ip-address": { "version": "8.1.0", @@ -29905,6 +30182,30 @@ "through": ">=2.2.7 <3" } }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, "jsprim": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", @@ -29927,6 +30228,31 @@ "object.assign": "^4.1.2" } }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "k-bucket": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/k-bucket/-/k-bucket-5.1.0.tgz", @@ -30502,11 +30828,52 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -31152,12 +31519,6 @@ "brace-expansion": "^1.1.7" } }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -31289,9 +31650,9 @@ "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==" }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "multiaddr": { "version": "10.0.1", @@ -31614,6 +31975,36 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, + "nise": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz", + "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": ">=5", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, "no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -34045,6 +34436,43 @@ } } }, + "sinon": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.0.tgz", + "integrity": "sha512-ugA6BFmE+WrJdh0owRZHToLd32Uw3Lxq6E6LtNRU+xTVBefx632h03Q7apXWRsRdZAJ41LB8aUfn2+O4jsDNMw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": "^9.1.2", + "@sinonjs/samsam": "^6.1.1", + "diff": "^5.0.0", + "nise": "^5.1.1", + "supports-color": "^7.2.0" + }, + "dependencies": { + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "slice-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", diff --git a/package.json b/package.json index bddb2a1d4d..636b21bf2e 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "openzeppelin-solidity": "3.4.2", "prettier": "^2.6.2", "request": "^2.88.2", + "sinon": "^14.0.0", "slugify": "^1.6.5", "solc": "0.7.6" }, @@ -80,6 +81,7 @@ "fastq": "^1.13.0", "fs-extra": "^10.0.0", "graphdb": "^2.0.0", + "ip": "^1.1.8", "it-concat": "^2.0.0", "it-length-prefixed": "^5.0.3", "it-map": "^1.0.6", @@ -89,6 +91,7 @@ "json-stable-stringify": "^1.0.1", "jsonld": "^5.2.0", "jsonschema": "^1.4.1", + "jsonwebtoken": "^8.5.1", "keccak256": "^1.0.6", "libp2p": "^0.32.4", "libp2p-bootstrap": "^0.13.0", @@ -99,6 +102,7 @@ "libp2p-tcp": "^0.17.2", "merkle-tools": "^1.4.1", "merkletreejs": "^0.2.32", + "ms": "^2.1.3", "multiformats": "^9.4.7", "mysql2": "^2.3.3", "n3": "^1.12.2", diff --git a/src/controller/http-api-router.js b/src/controller/http-api-router.js index f7f1af9c29..882d0f30fe 100644 --- a/src/controller/http-api-router.js +++ b/src/controller/http-api-router.js @@ -14,7 +14,9 @@ class HttpApiRouter { } async initialize() { + await this.initializeBeforeMiddlewares(); await this.initializeListeners(); + await this.initializeAfterMiddlewares(); await this.httpClientModuleManager.listen(); } @@ -77,6 +79,14 @@ class HttpApiRouter { this.infoController.handleHttpApiInfoRequest(req, res); }); } + + async initializeBeforeMiddlewares() { + await this.httpClientModuleManager.initializeBeforeMiddlewares(); + } + + async initializeAfterMiddlewares() { + await this.httpClientModuleManager.initializeAfterMiddlewares(); + } } module.exports = HttpApiRouter; diff --git a/src/modules/http-client/http-client-module-manager.js b/src/modules/http-client/http-client-module-manager.js index bff65e9ff1..564dcc22dd 100644 --- a/src/modules/http-client/http-client-module-manager.js +++ b/src/modules/http-client/http-client-module-manager.js @@ -1,6 +1,11 @@ const BaseModuleManager = require('../base-module-manager'); class HttpClientModuleManager extends BaseModuleManager { + constructor(ctx) { + super(ctx); + this.authService = ctx.authService; + } + getName() { return 'httpClient'; } @@ -28,6 +33,18 @@ class HttpClientModuleManager extends BaseModuleManager { return this.getImplementation().module.listen(); } } + + async initializeBeforeMiddlewares() { + if (this.initialized) { + return this.getImplementation().module.initializeBeforeMiddlewares(this.authService); + } + } + + async initializeAfterMiddlewares() { + if (this.initialized) { + return this.getImplementation().module.initializeAfterMiddlewares(this.authService); + } + } } module.exports = HttpClientModuleManager; diff --git a/src/modules/http-client/implementation/express-http-client.js b/src/modules/http-client/implementation/express-http-client.js index 95146bdb6c..6ffd5e0d84 100644 --- a/src/modules/http-client/implementation/express-http-client.js +++ b/src/modules/http-client/implementation/express-http-client.js @@ -3,29 +3,16 @@ const https = require('https'); const fs = require('fs-extra'); const fileUpload = require('express-fileupload'); const cors = require('cors'); -const requestValidationMiddleware = require('./request-validation-middleware'); -const rateLimiterMiddleware = require('./rate-limiter-middleware'); +const requestValidationMiddleware = require('./middleware/request-validation-middleware'); +const rateLimiterMiddleware = require('./middleware/rate-limiter-middleware'); +const authenticationMiddleware = require('./middleware/authentication-middleware'); +const authorizationMiddleware = require('./middleware/authorization-middleware'); class ExpressHttpClient { async initialize(config, logger) { this.config = config; this.logger = logger; this.app = express(); - - this.app.use( - fileUpload({ - createParentPath: true, - }), - ); - - this.app.use(cors()); - - this.app.use(express.json()); - - this.app.use((req, res, next) => { - this.logger.api(`${req.method}: ${req.url} request received`); - return next(); - }); } async get(route, callback, options) { @@ -65,6 +52,31 @@ class ExpressHttpClient { return middlewares; } + + async initializeBeforeMiddlewares(authService) { + this.app.use(authenticationMiddleware(authService)); + this.app.use(authorizationMiddleware(authService)); + this._initializeBaseMiddlewares(); + } + + async initializeAfterMiddlewares() { + // placeholder method for after middlewares + } + + _initializeBaseMiddlewares() { + this.app.use( + fileUpload({ + createParentPath: true, + }), + ); + + this.app.use(cors()); + this.app.use(express.json()); + this.app.use((req, res, next) => { + this.logger.api(`${req.method}: ${req.url} request received`); + return next(); + }); + } } module.exports = ExpressHttpClient; diff --git a/src/modules/http-client/implementation/middleware/authentication-middleware.js b/src/modules/http-client/implementation/middleware/authentication-middleware.js new file mode 100644 index 0000000000..970bb92c99 --- /dev/null +++ b/src/modules/http-client/implementation/middleware/authentication-middleware.js @@ -0,0 +1,37 @@ +const parseIp = (req) => { + let xForwardedFor; + let socketRemoteAddress; + + if (req.headers['x-forwarded-for']) { + xForwardedFor = req.headers['x-forwarded-for'].split(',').shift(); + } + + if (req.socket) { + socketRemoteAddress = req.socket.remoteAddress; + } + + return xForwardedFor || socketRemoteAddress; +}; + +module.exports = (authService) => async (req, res, next) => { + const operation = req.url.split('/')[1].toUpperCase(); + + if (authService.isPublicOperation(operation)) { + return next(); + } + + const ip = parseIp(req); + + const token = + req.headers.authorization && + req.headers.authorization.startsWith('Bearer ') && + req.headers.authorization.split(' ')[1]; + + const isAuthenticated = await authService.authenticate(ip, token); + + if (!isAuthenticated) { + return res.status(401).send('Unauthenticated.'); + } + + next(); +}; diff --git a/src/modules/http-client/implementation/middleware/authorization-middleware.js b/src/modules/http-client/implementation/middleware/authorization-middleware.js new file mode 100644 index 0000000000..f673749567 --- /dev/null +++ b/src/modules/http-client/implementation/middleware/authorization-middleware.js @@ -0,0 +1,22 @@ +const getToken = (req) => { + if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) { + return req.headers.authorization.split(' ')[1]; + } +}; + +module.exports = (authService) => async (req, res, next) => { + const operation = req.url.split('/')[1].toUpperCase(); + + if (authService.isPublicOperation(operation)) { + return next(); + } + + const token = getToken(req); + const isAuthorized = await authService.isAuthorized(token, operation); + + if (!isAuthorized) { + return res.status(403).send('Forbidden.'); + } + + next(); +}; diff --git a/src/modules/http-client/implementation/rate-limiter-middleware.js b/src/modules/http-client/implementation/middleware/rate-limiter-middleware.js similarity index 100% rename from src/modules/http-client/implementation/rate-limiter-middleware.js rename to src/modules/http-client/implementation/middleware/rate-limiter-middleware.js diff --git a/src/modules/http-client/implementation/request-validation-middleware.js b/src/modules/http-client/implementation/middleware/request-validation-middleware.js similarity index 100% rename from src/modules/http-client/implementation/request-validation-middleware.js rename to src/modules/http-client/implementation/middleware/request-validation-middleware.js diff --git a/src/modules/repository/implementation/sequelize/migrations/20220624103229-create-ability.js b/src/modules/repository/implementation/sequelize/migrations/20220624103229-create-ability.js new file mode 100644 index 0000000000..3598e9dccf --- /dev/null +++ b/src/modules/repository/implementation/sequelize/migrations/20220624103229-create-ability.js @@ -0,0 +1,29 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('ability', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER, + }, + name: { + type: Sequelize.STRING, + unique: true, + }, + created_at: { + allowNull: true, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + updated_at: { + allowNull: true, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + }); + }, + down: async (queryInterface) => { + await queryInterface.dropTable('ability'); + }, +}; diff --git a/src/modules/repository/implementation/sequelize/migrations/20220624103610-create-role.js b/src/modules/repository/implementation/sequelize/migrations/20220624103610-create-role.js new file mode 100644 index 0000000000..b00793cbdd --- /dev/null +++ b/src/modules/repository/implementation/sequelize/migrations/20220624103610-create-role.js @@ -0,0 +1,29 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('role', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER, + }, + name: { + type: Sequelize.STRING, + unique: true, + }, + created_at: { + allowNull: true, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + updated_at: { + allowNull: true, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + }); + }, + down: async (queryInterface) => { + await queryInterface.dropTable('role'); + }, +}; diff --git a/src/modules/repository/implementation/sequelize/migrations/20220624103615-create-user.js b/src/modules/repository/implementation/sequelize/migrations/20220624103615-create-user.js new file mode 100644 index 0000000000..13c40469d2 --- /dev/null +++ b/src/modules/repository/implementation/sequelize/migrations/20220624103615-create-user.js @@ -0,0 +1,36 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('user', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER, + }, + name: { + type: Sequelize.STRING, + unique: true, + }, + role_id: { + type: Sequelize.INTEGER, + references: { + model: 'role', + key: 'id', + }, + }, + created_at: { + allowNull: true, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + updated_at: { + allowNull: true, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + }); + }, + down: async (queryInterface) => { + await queryInterface.dropTable('user'); + }, +}; diff --git a/src/modules/repository/implementation/sequelize/migrations/20220624103658-create-token.js b/src/modules/repository/implementation/sequelize/migrations/20220624103658-create-token.js new file mode 100644 index 0000000000..ae0ad032a3 --- /dev/null +++ b/src/modules/repository/implementation/sequelize/migrations/20220624103658-create-token.js @@ -0,0 +1,44 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('token', { + id: { + allowNull: false, + primaryKey: true, + type: Sequelize.STRING, + }, + name: { + allowNull: false, + type: Sequelize.STRING, + unique: true, + }, + user_id: { + type: Sequelize.INTEGER, + references: { + model: 'user', + key: 'id', + }, + }, + expires_at: { + type: Sequelize.DATE, + allowNull: true, + }, + revoked: { + type: Sequelize.BOOLEAN, + defaultValue: false, + }, + created_at: { + allowNull: true, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + updated_at: { + allowNull: true, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + }); + }, + down: async (queryInterface) => { + await queryInterface.dropTable('token'); + }, +}; diff --git a/src/modules/repository/implementation/sequelize/migrations/20220624113659-create-role-ability.js b/src/modules/repository/implementation/sequelize/migrations/20220624113659-create-role-ability.js new file mode 100644 index 0000000000..0ce81a409e --- /dev/null +++ b/src/modules/repository/implementation/sequelize/migrations/20220624113659-create-role-ability.js @@ -0,0 +1,39 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('role_ability', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER, + }, + ability_id: { + type: Sequelize.INTEGER, + references: { + model: 'ability', + key: 'id', + }, + }, + role_id: { + type: Sequelize.INTEGER, + references: { + model: 'role', + key: 'id', + }, + }, + created_at: { + allowNull: true, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + updated_at: { + allowNull: true, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + }); + }, + down: async (queryInterface) => { + await queryInterface.dropTable('role_ability'); + }, +}; diff --git a/src/modules/repository/implementation/sequelize/migrations/20220628113824-add-predefined-auth-entities.js b/src/modules/repository/implementation/sequelize/migrations/20220628113824-add-predefined-auth-entities.js new file mode 100644 index 0000000000..fc45d9bed6 --- /dev/null +++ b/src/modules/repository/implementation/sequelize/migrations/20220628113824-add-predefined-auth-entities.js @@ -0,0 +1,78 @@ +const routes = [ + 'PUBLISH', + 'PROVISION', + 'UPDATE', + 'RESOLVE', + 'SEARCH', + 'SEARCH_ASSERTION', + 'QUERY', + 'PROOFS', + 'OPERATION_RESULT', + 'INFO', +]; + +module.exports = { + up: async (queryInterface) => { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.bulkInsert( + 'ability', + routes.map((r) => ({ name: r })), + { + transaction, + }, + ); + const [abilities] = await queryInterface.sequelize.query('SELECT id from ability', { + transaction, + }); + + await queryInterface.bulkInsert( + 'role', + [ + { + name: 'ADMIN', + }, + ], + { + transaction, + }, + ); + + const [[role]] = await queryInterface.sequelize.query( + "SELECT id from role where name='ADMIN'", + { + transaction, + }, + ); + + const roleAbilities = abilities.map((e) => ({ + ability_id: e.id, + role_id: role.id, + })); + + await queryInterface.bulkInsert('role_ability', roleAbilities, { transaction }); + + await queryInterface.bulkInsert( + 'user', + [ + { + name: 'node-runner', + role_id: role.id, + }, + ], + { transaction }, + ); + + transaction.commit(); + } catch (e) { + transaction.rollback(); + throw e; + } + }, + + down: async (queryInterface) => { + queryInterface.sequelize.query('TRUNCATE TABLE role_ability;'); + queryInterface.sequelize.query('TRUNCATE TABLE role;'); + queryInterface.sequelize.query('TRUNCATE TABLE ability;'); + }, +}; diff --git a/src/modules/repository/implementation/sequelize/models/ability.js b/src/modules/repository/implementation/sequelize/models/ability.js new file mode 100644 index 0000000000..880a1061c7 --- /dev/null +++ b/src/modules/repository/implementation/sequelize/models/ability.js @@ -0,0 +1,33 @@ +const { Model } = require('sequelize'); + +module.exports = (sequelize, DataTypes) => { + class Ability extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate() { + // define association here + } + } + Ability.init( + { + name: DataTypes.STRING, + createdAt: { + type: DataTypes.DATE, + field: 'created_at', + }, + updatedAt: { + type: DataTypes.DATE, + field: 'updated_at', + }, + }, + { + sequelize, + modelName: 'Ability', + underscored: true, + }, + ); + return Ability; +}; diff --git a/src/modules/repository/implementation/sequelize/models/role-ability.js b/src/modules/repository/implementation/sequelize/models/role-ability.js new file mode 100644 index 0000000000..d806544cb2 --- /dev/null +++ b/src/modules/repository/implementation/sequelize/models/role-ability.js @@ -0,0 +1,34 @@ +const { Model } = require('sequelize'); + +module.exports = (sequelize, DataTypes) => { + class RoleAbility extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + RoleAbility.hasOne(models.Ability, { as: 'ability' }); + RoleAbility.hasOne(models.Role, { as: 'role' }); + } + } + + RoleAbility.init( + { + createdAt: { + type: DataTypes.DATE, + field: 'created_at', + }, + updatedAt: { + type: DataTypes.DATE, + field: 'updated_at', + }, + }, + { + sequelize, + modelName: 'RoleAbility', + underscored: true, + }, + ); + return RoleAbility; +}; diff --git a/src/modules/repository/implementation/sequelize/models/role.js b/src/modules/repository/implementation/sequelize/models/role.js new file mode 100644 index 0000000000..2ee2f8f5e1 --- /dev/null +++ b/src/modules/repository/implementation/sequelize/models/role.js @@ -0,0 +1,37 @@ +const { Model } = require('sequelize'); + +module.exports = (sequelize, DataTypes) => { + class Role extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + Role.belongsToMany(models.Ability, { + as: 'abilities', + foreignKey: 'ability_id', + through: models.RoleAbility, + }); + } + } + Role.init( + { + name: DataTypes.STRING, + createdAt: { + type: DataTypes.DATE, + field: 'created_at', + }, + updatedAt: { + type: DataTypes.DATE, + field: 'updated_at', + }, + }, + { + sequelize, + modelName: 'Role', + underscored: true, + }, + ); + return Role; +}; diff --git a/src/modules/repository/implementation/sequelize/models/token.js b/src/modules/repository/implementation/sequelize/models/token.js new file mode 100644 index 0000000000..7cf92e64bd --- /dev/null +++ b/src/modules/repository/implementation/sequelize/models/token.js @@ -0,0 +1,45 @@ +const { Model } = require('sequelize'); + +module.exports = (sequelize, DataTypes) => { + class Token extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + Token.belongsTo(models.User, { as: 'user' }); + } + } + Token.init( + { + id: { type: DataTypes.STRING, primaryKey: true }, + revoked: DataTypes.BOOLEAN, + userId: { + type: DataTypes.INTEGER, + field: 'user_id', + }, + name: { + type: DataTypes.STRING, + }, + expiresAt: { + type: DataTypes.DATE, + field: 'expires_at', + }, + createdAt: { + type: DataTypes.DATE, + field: 'created_at', + }, + updatedAt: { + type: DataTypes.DATE, + field: 'updated_at', + }, + }, + { + sequelize, + modelName: 'Token', + underscored: true, + }, + ); + return Token; +}; diff --git a/src/modules/repository/implementation/sequelize/models/user.js b/src/modules/repository/implementation/sequelize/models/user.js new file mode 100644 index 0000000000..7daf0d947e --- /dev/null +++ b/src/modules/repository/implementation/sequelize/models/user.js @@ -0,0 +1,37 @@ +const { Model } = require('sequelize'); + +module.exports = (sequelize, DataTypes) => { + class User extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + User.hasMany(models.Token, { as: 'tokens' }); + User.hasOne(models.Role, { as: 'role' }); + } + } + User.init( + { + name: { + type: DataTypes.STRING, + unique: true, + }, + createdAt: { + type: DataTypes.DATE, + field: 'created_at', + }, + updatedAt: { + type: DataTypes.DATE, + field: 'updated_at', + }, + }, + { + sequelize, + modelName: 'User', + underscored: true, + }, + ); + return User; +}; diff --git a/src/modules/repository/implementation/sequelize/sequelize-repository.js b/src/modules/repository/implementation/sequelize/sequelize-repository.js index b1eb37723b..ce3782b7f8 100644 --- a/src/modules/repository/implementation/sequelize/sequelize-repository.js +++ b/src/modules/repository/implementation/sequelize/sequelize-repository.js @@ -292,6 +292,43 @@ class SequelizeRepository { }, }); } + + async getUser(username) { + return this.models.User.findOne({ + where: { + name: username, + }, + }); + } + + async saveToken(tokenId, userId, tokenName, expiresAt) { + return this.models.Token.create({ + id: tokenId, + userId, + expiresAt, + name: tokenName, + }); + } + + async isTokenRevoked(tokenId) { + const token = await this.models.Token.findByPk(tokenId); + + return token && token.revoked; + } + + async getTokenAbilities(tokenId) { + const abilities = await this.models.sequelize.query( + `SELECT a.name FROM token t +INNER JOIN user u ON t.user_id = u.id +INNER JOIN role r ON u.role_id = u.id +INNER JOIN role_ability ra on r.id = ra.role_id +INNER JOIN ability a on ra.ability_id = a.id +WHERE t.id=$tokenId;`, + { bind: { tokenId }, type: Sequelize.QueryTypes.SELECT }, + ); + + return abilities.map((e) => e.name); + } } module.exports = SequelizeRepository; diff --git a/src/modules/repository/repository-module-manager.js b/src/modules/repository/repository-module-manager.js index 9942349fa7..1c217d5412 100644 --- a/src/modules/repository/repository-module-manager.js +++ b/src/modules/repository/repository-module-manager.js @@ -162,6 +162,30 @@ class RepositoryModuleManager extends BaseModuleManager { return this.getImplementation().module.destroyEvents(ids); } } + + async getUser(username) { + if (this.initialized) { + return this.getImplementation().module.getUser(username); + } + } + + async saveToken(tokenId, userId, tokenName, expiresAt) { + if (this.initialized) { + return this.getImplementation().module.saveToken(tokenId, userId, tokenName, expiresAt); + } + } + + async isTokenRevoked(tokenId) { + if (this.initialized) { + return this.getImplementation().module.isTokenRevoked(tokenId); + } + } + + async getTokenAbilities(tokenId) { + if (this.initialized) { + return this.getImplementation().module.getTokenAbilities(tokenId); + } + } } module.exports = RepositoryModuleManager; diff --git a/src/service/auth-service.js b/src/service/auth-service.js new file mode 100644 index 0000000000..d1a7846de8 --- /dev/null +++ b/src/service/auth-service.js @@ -0,0 +1,145 @@ +const ipLib = require('ip'); +const jwtUtil = require('./util/jwt-util'); + +module.exports = class AuthService { + constructor(ctx) { + this._authConfig = ctx.config.auth; + this._repository = ctx.repositoryModuleManager; + this._logger = ctx.logger; + } + + /** + * Authenticate users based on provided ip and token + * @param ip + * @param token + * @returns {boolean} + */ + async authenticate(ip, token) { + const isWhitelisted = this._isIpWhitelisted(ip); + const isTokenValid = await this._isTokenValid(token); + + const isAuthenticated = isWhitelisted && isTokenValid; + + if (!isAuthenticated) { + this._logMessage('Received unauthenticated request.'); + } + + return isAuthenticated; + } + + /** + * Checks whether user whose token is provided has abilities for system operation + * @param token + * @param systemOperation + * @returns {Promise} + */ + async isAuthorized(token, systemOperation) { + if (!this._authConfig.tokenBasedAuthEnabled) { + return true; + } + + const tokenId = jwtUtil.getPayload(token).jti; + const abilities = await this._repository.getTokenAbilities(tokenId); + + const isAuthorized = abilities.includes(systemOperation); + + const logMessage = isAuthorized + ? `Token ${tokenId} is successfully authenticated and authorized.` + : `Received unauthorized request.`; + + this._logMessage(logMessage); + + return isAuthorized; + } + + /** + * Determines whether operation is listed in config.auth.publicOperations + * @param operationName + * @returns {boolean} + */ + isPublicOperation(operationName) { + if (!Array.isArray(this._authConfig.publicOperations)) { + return false; + } + + return this._authConfig.publicOperations.includes(operationName); + } + + /** + * Validates token structure and revoked status + * If ot-node is configured not to do a token based auth, it will return true + * @param token + * @returns {boolean} + * @private + */ + async _isTokenValid(token) { + if (!this._authConfig.tokenBasedAuthEnabled) { + return true; + } + + if (!jwtUtil.validateJWT(token)) { + return false; + } + + const isRevoked = await this._isTokenRevoked(token); + + return isRevoked !== null && !isRevoked; + } + + /** + * Checks whether provided ip is whitelisted in config + * Returns false if ip based auth is disabled + * @param reqIp + * @returns {boolean} + * @private + */ + _isIpWhitelisted(reqIp) { + if (!this._authConfig.ipBasedAuthEnabled) { + return true; + } + + for (const whitelistedIp of this._authConfig.ipWhitelist) { + let isEqual = false; + + try { + isEqual = ipLib.isEqual(reqIp, whitelistedIp); + } catch (e) { + // if ip is not valid IP isEqual should remain false + } + + if (isEqual) { + return true; + } + } + + return false; + } + + /** + * Checks whether provided token is revoked + * Returns false if token based auth is disabled + * @param token + * @returns {Promise|boolean} + * @private + */ + _isTokenRevoked(token) { + if (!this._authConfig.tokenBasedAuthEnabled) { + return false; + } + + const tokenId = jwtUtil.getPayload(token).jti; + + return this._repository.isTokenRevoked(tokenId); + } + + /** + * Logs message if loggingEnabled is set to true + * @param message + * @private + */ + _logMessage(message) { + if (this._authConfig.loggingEnabled) { + this._logger.info(`[AUTH] ${message}`); + } + } +}; diff --git a/src/service/util/jwt-util.js b/src/service/util/jwt-util.js new file mode 100644 index 0000000000..5308cae5a6 --- /dev/null +++ b/src/service/util/jwt-util.js @@ -0,0 +1,67 @@ +const jwt = require('jsonwebtoken'); +const { validate } = require('uuid'); + +class JwtUtil { + constructor() { + this._secret = process.env.JWT_SECRET; + } + + /** + * Generates new JWT token + * @param uuid uuid from token table + * @param expiresIn optional parameter. accepts values for ms package (https://www.npmjs.com/package/ms) + * @returns {string|null} + */ + generateJWT(uuid, expiresIn = null) { + if (!validate(uuid)) { + return null; + } + + const options = { + jwtid: uuid, + }; + + if (expiresIn) { + options.expiresIn = expiresIn; + } + + return jwt.sign({}, this._secret, options); + } + + /** + * Validates JWT token + * @param {string} token + * @returns {boolean} + */ + validateJWT(token) { + try { + jwt.verify(token, this._secret); + } catch (e) { + return false; + } + + return true; + } + + /** + * Returns JWT payload + * @param {string} token + * @returns {*} + */ + getPayload(token) { + return jwt.decode(token); + } + + /** + * Decodes token + * @param token + * @returns {{payload: any, signature: *, header: *}|*} + */ + decode(token) { + return jwt.decode(token, { complete: true }); + } +} + +const jwtUtil = new JwtUtil(); + +module.exports = jwtUtil; diff --git a/test/unit/middleware/authentication-middleware.test.js b/test/unit/middleware/authentication-middleware.test.js new file mode 100644 index 0000000000..4f332a0511 --- /dev/null +++ b/test/unit/middleware/authentication-middleware.test.js @@ -0,0 +1,85 @@ +const sinon = require('sinon'); +const { describe, it, afterEach } = require('mocha'); +const { expect } = require('chai'); + +const authenticationMiddleware = require('../../../src/modules/http-client/implementation/middleware/authentication-middleware'); +const AuthService = require('../../../src/service/auth-service'); + +describe('authentication middleware test', async () => { + const sandbox = sinon.createSandbox(); + + const getAuthService = (options) => + sandbox.createStubInstance(AuthService, { + authenticate: options.isAuthenticated, + isPublicOperation: options.isPublicOperation, + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('calls next if isPublic evaluated to true', async () => { + const middleware = authenticationMiddleware( + getAuthService({ + isPublicOperation: true, + }), + ); + + const req = { headers: { authorization: 'Bearer token' }, url: '/publish' }; + + const spySend = sandbox.spy(); + const spyStatus = sandbox.spy(() => ({ send: spySend })); + + const nextSpy = sandbox.spy(); + await middleware(req, { status: spyStatus }, nextSpy); + + expect(nextSpy.calledOnce).to.be.true; + expect(spyStatus.notCalled).to.be.true; + expect(spySend.notCalled).to.be.true; + }); + + it('calls next if isAuthenticated is evaluated as true', async () => { + const middleware = authenticationMiddleware( + getAuthService({ + isPublicOperation: false, + isAuthenticated: true, + }), + ); + + const req = { headers: { authorization: 'Bearer token' }, url: '/publish' }; + + const spySend = sandbox.spy(); + const spyStatus = sandbox.spy(() => ({ send: spySend })); + + const nextSpy = sandbox.spy(); + await middleware(req, { status: spyStatus }, nextSpy); + + expect(nextSpy.calledOnce).to.be.true; + expect(spyStatus.notCalled).to.be.true; + expect(spySend.notCalled).to.be.true; + }); + + it('returns 401 if isAuthenticated is evaluated as false', async () => { + const middleware = authenticationMiddleware( + getAuthService({ + isPublicOperation: false, + isAuthenticated: false, + }), + ); + + const req = { headers: { authorization: 'Bearer token' }, url: '/publish' }; + + const spySend = sandbox.spy(); + const spyStatus = sandbox.spy(() => ({ send: spySend })); + const spyNext = sandbox.spy(); + + await middleware(req, { status: spyStatus }, spyNext); + + const [statusCode] = spyStatus.args[0]; + + expect(statusCode).to.be.eq(401); + expect(spyStatus.calledOnce).to.be.true; + expect(spySend.calledOnce).to.be.true; + expect(spyNext.notCalled).to.be.true; + }); +}); diff --git a/test/unit/middleware/authorization-middleware.test.js b/test/unit/middleware/authorization-middleware.test.js new file mode 100644 index 0000000000..c805d9e5a8 --- /dev/null +++ b/test/unit/middleware/authorization-middleware.test.js @@ -0,0 +1,85 @@ +const sinon = require('sinon'); +const { describe, it, afterEach } = require('mocha'); +const { expect } = require('chai'); + +const authorizationMiddleware = require('../../../src/modules/http-client/implementation/middleware/authorization-middleware'); +const AuthService = require('../../../src/service/auth-service'); + +describe('authentication middleware test', async () => { + const sandbox = sinon.createSandbox(); + + const getAuthService = (options) => + sandbox.createStubInstance(AuthService, { + authenticate: options.isAuthenticated, + isAuthorized: options.isAuthorized, + isPublicOperation: options.isPublicOperation, + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('calls next if isPublicOperation is resolved to true', async () => { + const middleware = authorizationMiddleware( + getAuthService({ + isPublicOperation: true, + }), + ); + + const req = { headers: { authorization: 'Bearer token' }, url: '/publish' }; + + const spySend = sandbox.spy(); + const spyStatus = sandbox.spy(() => ({ send: spySend })); + + const nextSpy = sandbox.spy(); + await middleware(req, { status: spyStatus }, nextSpy); + + expect(nextSpy.calledOnce).to.be.true; + expect(spyStatus.notCalled).to.be.true; + expect(spySend.notCalled).to.be.true; + }); + + it('calls next if isAuthenticated is evaluated as true', async () => { + const middleware = authorizationMiddleware( + getAuthService({ + isPublicOperation: true, + }), + ); + + const req = { headers: { authorization: 'Bearer token' }, url: '/publish' }; + + const spySend = sandbox.spy(); + const spyStatus = sandbox.spy(() => ({ send: spySend })); + + const nextSpy = sandbox.spy(); + await middleware(req, { status: spyStatus }, nextSpy); + + expect(nextSpy.calledOnce).to.be.true; + expect(spyStatus.notCalled).to.be.true; + expect(spySend.notCalled).to.be.true; + }); + + it('returns 403 if isAuthenticated is evaluated as false', async () => { + const middleware = authorizationMiddleware( + getAuthService({ + isPublicOperation: false, + isAuthenticated: false, + }), + ); + + const req = { headers: { authorization: 'Bearer token' }, url: '/publish' }; + + const spySend = sandbox.spy(); + const spyStatus = sandbox.spy(() => ({ send: spySend })); + const spyNext = sandbox.spy(); + + await middleware(req, { status: spyStatus }, spyNext); + + const [statusCode] = spyStatus.args[0]; + + expect(statusCode).to.be.eq(403); + expect(spyStatus.calledOnce).to.be.true; + expect(spySend.calledOnce).to.be.true; + expect(spyNext.notCalled).to.be.true; + }); +}); diff --git a/test/unit/service/auth-service.test.js b/test/unit/service/auth-service.test.js new file mode 100644 index 0000000000..a18b6b74d4 --- /dev/null +++ b/test/unit/service/auth-service.test.js @@ -0,0 +1,321 @@ +require('dotenv').config(); +const { expect } = require('chai'); +const { describe, it, afterEach } = require('mocha'); +const uuid = require('uuid').v4; +const sinon = require('sinon'); + +const AuthService = require('../../../src/service/auth-service'); +const jwtUtil = require('../../../src/service/util/jwt-util'); +const RepositoryModuleManager = require('../../../src/modules/repository/repository-module-manager'); + +const whitelistedIps = [ + '::1', + '127.0.0.1', + '54.31.28.8', + 'a3c6:3c39:492c:831b:d1a1:7944:b984:f32a', +]; +const invalidIps = ['247.8.32.50', null, undefined, {}, [], true, false, 123, '123...', NaN]; +const invalidTokens = ['token', 12345, {}, [], undefined, null, true, false, '123.321.32', NaN]; + +const tests = [ + { + ipAuthEnabled: false, + tokenAuthEnabled: false, + ipValid: false, + tokenValid: false, + tokenRevoked: false, + tokenExpired: false, + expected: true, + }, + { + ipAuthEnabled: true, + tokenAuthEnabled: false, + ipValid: false, + tokenValid: false, + tokenRevoked: false, + tokenExpired: false, + expected: false, + }, + { + ipAuthEnabled: true, + tokenAuthEnabled: false, + ipValid: true, + tokenValid: false, + tokenRevoked: false, + tokenExpired: false, + expected: true, + }, + { + ipAuthEnabled: false, + tokenAuthEnabled: true, + ipValid: false, + tokenValid: false, + tokenRevoked: false, + tokenExpired: false, + expected: false, + }, + { + ipAuthEnabled: false, + tokenAuthEnabled: true, + ipValid: true, + tokenValid: false, + tokenRevoked: false, + tokenExpired: false, + expected: false, + }, + { + ipAuthEnabled: false, + tokenAuthEnabled: true, + ipValid: false, + tokenValid: true, + tokenRevoked: false, + tokenExpired: false, + expected: true, + }, + { + ipAuthEnabled: false, + tokenAuthEnabled: true, + ipValid: false, + tokenValid: true, + tokenRevoked: true, + tokenExpired: false, + expected: false, + }, + { + ipAuthEnabled: false, + tokenAuthEnabled: true, + ipValid: false, + tokenValid: true, + tokenRevoked: false, + tokenExpired: true, + expected: false, + }, + { + ipAuthEnabled: false, + tokenAuthEnabled: true, + ipValid: false, + tokenValid: true, + tokenRevoked: true, + tokenExpired: true, + expected: false, + }, + { + ipAuthEnabled: true, + tokenAuthEnabled: true, + ipValid: false, + tokenValid: false, + tokenRevoked: false, + tokenExpired: false, + expected: false, + }, + { + ipAuthEnabled: true, + tokenAuthEnabled: true, + ipValid: true, + tokenValid: false, + tokenRevoked: false, + tokenExpired: false, + expected: false, + }, + { + ipAuthEnabled: true, + tokenAuthEnabled: true, + ipValid: true, + tokenValid: true, + tokenRevoked: false, + tokenExpired: false, + expected: true, + }, + { + ipAuthEnabled: true, + tokenAuthEnabled: true, + ipValid: true, + tokenValid: true, + tokenRevoked: true, + tokenExpired: false, + expected: false, + }, + { + ipAuthEnabled: true, + tokenAuthEnabled: true, + ipValid: true, + tokenValid: true, + tokenRevoked: false, + tokenExpired: true, + expected: false, + }, + { + ipAuthEnabled: true, + tokenAuthEnabled: true, + ipValid: true, + tokenValid: true, + tokenRevoked: true, + tokenExpired: true, + expected: false, + }, +]; + +const configObj = { + auth: { + ipWhitelist: whitelistedIps, + publicOperations: ['QUERY'], + }, +}; + +const getConfig = (ipAuthEnabled, tokenAuthEnabled) => { + const configClone = JSON.parse(JSON.stringify(configObj)); + + configClone.auth.ipBasedAuthEnabled = ipAuthEnabled; + configClone.auth.tokenBasedAuthEnabled = tokenAuthEnabled; + + return configClone; +}; + +const getRepository = (isTokenRevoked, tokenAbilitiesValid) => + sinon.createStubInstance(RepositoryModuleManager, { + isTokenRevoked, + getTokenAbilities: tokenAbilitiesValid ? ['QUERY', 'PUBLISH', 'SEARCH'] : [], + }); + +const getIps = (isValid) => { + if (isValid) { + return whitelistedIps; + } + + return invalidIps; +}; + +const getTokens = (isValid, isExpired) => { + if (isValid) { + if (isExpired) { + return [jwtUtil.generateJWT(uuid(), '-2d')]; + } + return [jwtUtil.generateJWT(uuid())]; + } + + return invalidTokens; +}; + +describe('authenticate()', async () => { + afterEach(() => { + sinon.restore(); + }); + + for (const t of tests) { + let testText = ''; + + for (const field in t) { + if (field === 'expected') { + testText += `${field.toUpperCase()}: ${t[field]}`; + } else { + testText += `${field}: ${t[field]} | `; + } + } + + it(testText, async () => { + const config = getConfig(t.ipAuthEnabled, t.tokenAuthEnabled); + const repositoryModuleManager = getRepository(t.tokenRevoked); + const ips = getIps(t.ipValid); + const tokens = getTokens(t.tokenValid, t.tokenExpired); + const authService = new AuthService({ config, repositoryModuleManager }); + + for (const ip of ips) { + for (const token of tokens) { + // eslint-disable-next-line no-await-in-loop + const isAuthenticated = await authService.authenticate(ip, token); + expect(isAuthenticated).to.be.equal(t.expected); + } + } + }); + } + + it('returns false if token is valid but is not found in the database', async () => { + const config = getConfig(false, true); + const repositoryModuleManager = getRepository(null, true); + const [token] = getTokens(true); + const authService = new AuthService({ config, repositoryModuleManager }); + + const isAuthenticated = await authService.authenticate('', token); + expect(isAuthenticated).to.be.false; + }); +}); + +describe('isAuthorized()', async () => { + afterEach(() => { + sinon.restore(); + }); + + it('returns true if tokenBasedAuthentication is disabled', async () => { + const config = getConfig(false, false); + const authService = new AuthService({ config }); + + const isAuthorized = await authService.isAuthorized(null, null); + expect(isAuthorized).to.be.equal(true); + }); + + it('returns true if user has ability to perform an action', async () => { + const config = getConfig(false, true); + const repositoryModuleManager = getRepository(false, true); + const jwt = jwtUtil.generateJWT(uuid()); + const authService = new AuthService({ config, repositoryModuleManager }); + + const isAuthorized = await authService.isAuthorized(jwt, 'QUERY'); + expect(isAuthorized).to.be.equal(true); + }); + + it("returns false if user doesn't have ability to perform an action", async () => { + const config = getConfig(false, true); + const jwt = jwtUtil.generateJWT(uuid()); + + const authService = new AuthService({ + config, + repositoryModuleManager: getRepository(false, true), + }); + const isAuthorized = await authService.isAuthorized(jwt, 'OPERATION'); + expect(isAuthorized).to.be.equal(false); + }); + + it('returns false if user roles are not found', async () => { + const config = getConfig(false, true); + const jwt = jwtUtil.generateJWT(uuid()); + + const authService = new AuthService({ + config, + repositoryModuleManager: getRepository(false, false), + }); + + const isAuthorized = await authService.isAuthorized(jwt, 'PUBLISH'); + expect(isAuthorized).to.be.equal(false); + }); +}); + +describe('isPublicOperation()', async () => { + afterEach(() => { + sinon.restore(); + }); + + it('returns true if route is public', async () => { + const config = getConfig(false, false); + const authService = new AuthService({ config }); + + const isPublic = authService.isPublicOperation('QUERY'); + expect(isPublic).to.be.equal(true); + }); + + it('returns false if route is not public', async () => { + const config = getConfig(false, false, true); + const authService = new AuthService({ config }); + + const isPublic = authService.isPublicOperation('PUBLISH'); + expect(isPublic).to.be.equal(false); + }); + + it('returns false if public routes are not defined', async () => { + const config = getConfig(false, false, true); + config.auth.publicOperations = undefined; + const authService = new AuthService({ config }); + + const isPublic = authService.isPublicOperation('PUBLISH'); + expect(isPublic).to.be.equal(false); + }); +}); diff --git a/test/unit/service/util/jwt-util.test.js b/test/unit/service/util/jwt-util.test.js new file mode 100644 index 0000000000..eb4577403a --- /dev/null +++ b/test/unit/service/util/jwt-util.test.js @@ -0,0 +1,108 @@ +require('dotenv').config(); +const uuid = require('uuid').v4; +const { expect } = require('chai'); + +const { describe, it } = require('mocha'); + +const getPayload = (token) => { + const b64Payload = token.split('.')[1]; + + return JSON.parse(Buffer.from(b64Payload, 'base64').toString()); +}; + +const jwtUtil = require('../../../../src/service/util/jwt-util'); + +const nonJwts = [ + '123', + 214214124124, + 'header.payload.signature', + null, + undefined, + true, + false, + {}, + [], +]; + +describe('Auth JWT generation', async () => { + it('generates JWT token with tokenId payload', () => { + const tokenId = uuid(); + const token = jwtUtil.generateJWT(tokenId); + + expect(token).not.be.null; + expect(getPayload(token).jti).to.be.equal(tokenId); + }); + + it('generates null if invalid tokenId is provided', () => { + const nonUuids = [true, false, undefined, null, {}, [], 'string', 123]; + + for (const val of nonUuids) { + const token = jwtUtil.generateJWT(val); + expect(token).to.be.null; + } + }); + + it('generates payload without expiration date if expiresIn argument is not provided', () => { + const token = jwtUtil.generateJWT(uuid()); + expect(getPayload(token).exp).to.be.undefined; + }); + + it('generates payload with expiration date if expiresIn argument is provided', () => { + const token = jwtUtil.generateJWT(uuid(), '2d'); + expect(getPayload(token).exp).to.be.ok; + }); +}); + +describe('JWT token validation', async () => { + it('returns true if JWT is valid', async () => { + const token = jwtUtil.generateJWT(uuid()); + expect(jwtUtil.validateJWT(token)).to.be.true; + }); + + it('returns true if JWT is expired', async () => { + const token = jwtUtil.generateJWT(uuid(), '-2d'); + expect(jwtUtil.validateJWT(token)).to.be.false; + }); + + it('returns false if JWT is not valid', async () => { + const token = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; + expect(jwtUtil.validateJWT(token)).to.be.false; + }); + + it('returns false if non JWT value is passed', async () => { + for (const val of nonJwts) { + expect(jwtUtil.validateJWT(val)).to.be.false; + } + }); +}); + +describe('JWT payload extracting', async () => { + it('returns JWT payload if valid token is provided', async () => { + const token = jwtUtil.generateJWT(uuid()); + + expect(jwtUtil.getPayload(token)).to.be.ok; + }); + + it('returns null if invalid token is provided', async () => { + for (const val of nonJwts) { + expect(jwtUtil.getPayload(val)).to.be.null; + } + }); +}); + +describe('JWT decoding', async () => { + it('returns decoded JWT (header, payload, signature) if valid token is provided', async () => { + const token = jwtUtil.generateJWT(uuid()); + + expect(jwtUtil.decode(token).header).to.be.ok; + expect(jwtUtil.decode(token).payload).to.be.ok; + expect(jwtUtil.decode(token).signature).to.be.ok; + }); + + it('returns null if invalid token is provided', async () => { + for (const val of nonJwts) { + expect(jwtUtil.decode(val)).to.be.null; + } + }); +}); diff --git a/tools/token-generation.js b/tools/token-generation.js new file mode 100644 index 0000000000..510d4eb10f --- /dev/null +++ b/tools/token-generation.js @@ -0,0 +1,174 @@ +/* eslint no-console: 0 */ +const ms = require('ms'); +const DeepExtend = require('deep-extend'); +const rc = require('rc'); +const fs = require('fs-extra'); +const uuid = require('uuid').v4; +const Logger = require('../modules/logger/logger'); +const configjson = require('../config/config.json'); +const pjson = require('../package.json'); +const RepositoryModuleManager = require('../src/modules/repository/repository-module-manager'); +require('dotenv').config(); +const jwtUtil = require('../src/service/util/jwt-util'); + +const getLogger = () => new Logger('silent', false); +let repository; + +const getConfig = () => { + let config; + let userConfig; + + if (process.env.USER_CONFIG_PATH) { + const configurationFilename = process.env.USER_CONFIG_PATH; + const pathSplit = configurationFilename.split('/'); + userConfig = JSON.parse(fs.readFileSync(configurationFilename)); + userConfig.configFilename = pathSplit[pathSplit.length - 1]; + } + + const defaultConfig = JSON.parse(JSON.stringify(configjson[process.env.NODE_ENV])); + + if (userConfig) { + config = DeepExtend(defaultConfig, userConfig); + } else { + config = rc(pjson.name, defaultConfig); + } + + if (!config.configFilename) { + // set default user configuration filename + config.configFilename = '.origintrail_noderc'; + } + return config; +}; + +const loadRepository = async () => { + repository = new RepositoryModuleManager({ logger: getLogger(), config: getConfig() }); + await repository.initialize(); +}; + +/** + * Returns argument from argv + * @param argName + * @returns {string|null} + */ +const getArg = (argName) => { + const args = process.argv; + const arg = args.find((a) => a.startsWith(argName)); + + if (!arg) { + return null; + } + + const argSplit = arg.split('='); + + if (!arg || argSplit.length < 2 || !argSplit[1]) { + return null; + } + + return argSplit[1]; +}; +/** + * Returns user's name from arguments + * @returns {string} + */ +const getUserFromArgs = () => { + const arg = getArg('--user'); + + if (!arg) { + return 'node-runner'; + } + + return arg; +}; + +/** + * Returns expiresAt from arguments + * If no expiresAt is provided, null is returned + * Expressed in seconds or a string describing a time span zeit/ms + * @returns {string|null} + */ +const getExpiresInArg = () => { + const arg = getArg('--expiresIn'); + + if (!arg) { + return null; + } + + if (!ms(arg)) { + console.log('\x1b[31m[ERROR]\x1b[0m Invalid value for expiresIn argument'); + process.exit(1); + } + + return arg; +}; + +/** + * Returns expiresAt from arguments + * If no expiresAt is provided, null is returned + * Expressed in seconds or a string describing a time span zeit/ms + * @returns {string|null} + */ +const getTokenName = () => { + const arg = getArg('--tokenName'); + + if (!arg) { + console.log('\x1b[31m[ERROR]\x1b[0m Missing mandatory tokenName argument.'); + process.exit(1); + } + + return arg; +}; + +const saveTokenData = async (tokenId, userId, tokenName, expiresIn) => { + let expiresAt = null; + + if (expiresIn) { + const time = new Date().getTime() + ms(expiresIn); + expiresAt = new Date(time); + } + + await repository.saveToken(tokenId, userId, tokenName, expiresAt); +}; + +const printMessage = (token, hasExpiryDate) => { + console.log('\x1b[32mAccess token successfully created.\x1b[0m '); + + if (!hasExpiryDate) { + console.log('\x1b[33m[WARNING] Created token has no expiry date. \x1b[0m '); + } + + console.log(token); + console.log( + '\x1b[32mMake sure to copy your personal access token now. You won’t be able to see it again!\x1b[0m ', + ); +}; + +const getUserId = async (username) => { + const user = await repository.getUser(username); + + if (!user) { + console.log(`\x1b[31m[ERROR]\x1b[0m User ${username} doesn't exist.`); + process.exit(1); + } + + return user.id; +}; + +const generateToken = async () => { + const username = getUserFromArgs(); + const expiresIn = getExpiresInArg(); + const tokenName = getTokenName(); + + await loadRepository(); + + const userId = await getUserId(username); + const tokenId = uuid(); + + await saveTokenData(tokenId, userId, tokenName, expiresIn); + + const token = jwtUtil.generateJWT(tokenId, expiresIn); + + printMessage(token, expiresIn); + process.exit(0); +}; + +generateToken();