diff --git a/CHANGELOG.md b/CHANGELOG.md index 8099d1800ac3f0c6fac0c502d8e867c08655356c..8bd5078f33a4ad8a9679f09780a61348a04e8868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Added +- Correct OAuth2 access handling - Use ALS for request-agnostic context injection - Add performance monitoring to queue jobs - Use queue for sending password reset emails diff --git a/database/migrations/20210000000005-create-oidc-entities-table.js b/database/migrations/20210000000005-create-oidc-entities-table.js new file mode 100644 index 0000000000000000000000000000000000000000..d53a279b325359efd7a5497c98107df65e9eeefa --- /dev/null +++ b/database/migrations/20210000000005-create-oidc-entities-table.js @@ -0,0 +1,71 @@ +module.exports = { + up: (migration, Types) => { + return migration.sequelize.transaction(async t => { + await migration.createTable('oidc_entities', { + id: { + type: Types.TEXT, + primaryKey: true, + allowNull: false, + }, + related_user_id: { + type: Types.UUID, + allowNull: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + references: { + model: 'users', + key: 'id', + }, + }, + grant_id: { + type: Types.TEXT, + allowNull: true, + index: true, + }, + user_code: { + type: Types.TEXT, + allowNull: true, + index: true, + }, + uid: { + type: Types.TEXT, + allowNull: true, + index: true, + }, + data: { + type: Types.JSONB, + allowNull: false, + }, + expires_at: { + type: Types.DATE, + allowNull: true, + }, + consumed_at: { + type: Types.DATE, + allowNull: true, + }, + meta: { + type: Types.JSONB, + defaultValue: {}, + allowNull: false, + }, + created_at: { + type: Types.DATE, + defaultValue: Types.fn('now'), + allowNull: false, + }, + updated_at: { + type: Types.DATE, + defaultValue: Types.fn('now'), + allowNull: false, + }, + }, { transaction: t }) + await migration.addIndex('oidc_entities', ['grant_id'], { transaction: t }) + await migration.addIndex('oidc_entities', ['user_code'], { transaction: t }) + await migration.addIndex('oidc_entities', ['uid'], { transaction: t }) + }) + }, + down: (migration, Types) => { + return migration.dropTable('oidc_entities') + }, +} diff --git a/package-lock.json b/package-lock.json index a2d9da7a5c3db490c61358954473d62e9a429355..15800f41045b724d1448575117ed9364f3c2ab16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "jose": "^3.6.1", "koa": "^2.13.0", "koa-bodyparser": "^4.3.0", + "koa-compose": "^4.1.0", "koa-compress": "^5.0.1", "koa-csrf": "^3.0.8", "koa-etag": "^3.0.0", @@ -46,6 +47,7 @@ "node-fetch": "^2.6.1", "nodemailer": "^6.4.17", "oauth2-server": "^3.1.1", + "oidc-provider": "^7.10.1", "pg": "^8.3.0", "pg-hstore": "^2.3.3", "pluralize": "^8.0.0", @@ -1590,6 +1592,17 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", + "integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, "node_modules/@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -1605,6 +1618,11 @@ "@types/node": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://npm.lcr.gr/@types%2fistanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -1629,6 +1647,14 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/keyv": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.3.tgz", + "integrity": "sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "14.0.22", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.22.tgz", @@ -1646,6 +1672,14 @@ "integrity": "sha512-6gOkRe7OIioWAXfnO/2lFiv+SJichKVSys1mSsgyrYHSEjk8Ctv4tSR/Odvnu+HWlH2C8j53dahU03XmQdd5fA==", "dev": true }, + "node_modules/@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.0", "resolved": "https://npm.lcr.gr/@types%2fstack-utils/-/stack-utils-2.0.0.tgz", @@ -1968,6 +2002,11 @@ "node": ">=0.10.0" } }, + "node_modules/async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2613,6 +2652,14 @@ "node": ">= 6.0.0" } }, + "node_modules/cacheable-lookup": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.0.4.tgz", + "integrity": "sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A==", + "engines": { + "node": ">=10.6.0" + } + }, "node_modules/cacheable-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", @@ -2850,7 +2897,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "dev": true, "dependencies": { "mimic-response": "^1.0.0" } @@ -3283,10 +3329,9 @@ } }, "node_modules/debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dependencies": { "ms": "2.1.2" }, @@ -3647,6 +3692,20 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "node_modules/ejs": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.6.tgz", + "integrity": "sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==", + "dependencies": { + "jake": "^10.6.1" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/emittery": { "version": "0.7.2", "resolved": "https://npm.lcr.gr/emittery/-/emittery-0.7.2.tgz", @@ -4173,6 +4232,14 @@ "resolved": "https://npm.lcr.gr/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, + "node_modules/filelist": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz", + "integrity": "sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ==", + "dependencies": { + "minimatch": "^3.0.4" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -4897,8 +4964,7 @@ "node_modules/http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" }, "node_modules/http-deceiver": { "version": "1.2.7", @@ -4965,6 +5031,18 @@ "npm": ">=1.3.7" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", @@ -5562,6 +5640,23 @@ "node": ">=8" } }, + "node_modules/jake": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz", + "integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==", + "dependencies": { + "async": "0.9.x", + "chalk": "^2.4.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/jest": { "version": "26.6.3", "resolved": "https://npm.lcr.gr/jest/-/jest-26.6.3.tgz", @@ -7852,18 +7947,18 @@ } }, "node_modules/koa": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.0.tgz", - "integrity": "sha512-i/XJVOfPw7npbMv67+bOeXr3gPqOAw6uh5wFyNs3QvJ47tUx3M3V9rIE0//WytY42MKz4l/MXKyGkQ2LQTfLUQ==", + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.4.tgz", + "integrity": "sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g==", "dependencies": { "accepts": "^1.3.5", "cache-content-type": "^1.0.0", "content-disposition": "~0.5.2", "content-type": "^1.0.4", "cookies": "~0.8.0", - "debug": "~3.1.0", + "debug": "^4.3.2", "delegates": "^1.0.0", - "depd": "^1.1.2", + "depd": "^2.0.0", "destroy": "^1.0.4", "encodeurl": "^1.0.2", "escape-html": "^1.0.3", @@ -7872,7 +7967,7 @@ "http-errors": "^1.6.3", "is-generator-function": "^1.0.7", "koa-compose": "^4.1.0", - "koa-convert": "^1.2.0", + "koa-convert": "^2.0.0", "on-finished": "^2.3.0", "only": "~0.0.2", "parseurl": "^1.3.2", @@ -7925,23 +8020,15 @@ } }, "node_modules/koa-convert": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", - "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", + "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", "dependencies": { "co": "^4.6.0", - "koa-compose": "^3.0.0" + "koa-compose": "^4.1.0" }, "engines": { - "node": ">= 4" - } - }, - "node_modules/koa-convert/node_modules/koa-compose": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", - "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", - "dependencies": { - "any-promise": "^1.1.0" + "node": ">= 10" } }, "node_modules/koa-csrf": { @@ -8060,18 +8147,13 @@ "ms": "^2.1.1" } }, - "node_modules/koa/node_modules/debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/koa/node_modules/ms": { + "node_modules/koa/node_modules/depd": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } }, "node_modules/latest-version": { "version": "5.1.0", @@ -8341,7 +8423,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, "engines": { "node": ">=4" } @@ -8480,6 +8561,17 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nanoid": { + "version": "3.1.30", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", + "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/nanomatch": { "version": "1.2.13", "resolved": "https://npm.lcr.gr/nanomatch/-/nanomatch-1.2.13.tgz", @@ -8881,6 +8973,238 @@ "resolved": "https://npm.lcr.gr/obuf/-/obuf-1.1.2.tgz", "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" }, + "node_modules/oidc-provider": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-7.10.1.tgz", + "integrity": "sha512-SRFVePq/ki76SbKB/vx7ach6mgc6iof2dtYOKMA88f3i1gZ6XKbqqxqyEVAj4n9L13nssUWJE9O4AyN8M0bNKA==", + "dependencies": { + "@koa/cors": "^3.1.0", + "cacheable-lookup": "^6.0.1", + "debug": "^4.3.2", + "ejs": "^3.1.6", + "got": "^11.8.2", + "jose": "^4.1.4", + "jsesc": "^3.0.2", + "koa": "^2.13.3", + "koa-compose": "^4.1.0", + "nanoid": "^3.1.28", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.1", + "paseto2": "npm:paseto@^2.1.3", + "quick-lru": "^5.1.1", + "raw-body": "^2.4.1" + }, + "engines": { + "node": "^12.19.0 || ^14.15.0 || ^16.13.0" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + }, + "optionalDependencies": { + "paseto3": "npm:paseto@^3.0.0" + } + }, + "node_modules/oidc-provider/node_modules/@sindresorhus/is": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz", + "integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/oidc-provider/node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/oidc-provider/node_modules/cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/oidc-provider/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/oidc-provider/node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/oidc-provider/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/oidc-provider/node_modules/got": { + "version": "11.8.2", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", + "integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.1", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/oidc-provider/node_modules/got/node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/oidc-provider/node_modules/jose": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.3.6.tgz", + "integrity": "sha512-A/JgZGUerqG2IMuxkUDBtZ4aTxg/l1Y+pt/QAAYiRAR3EFlxIE0Su0xdpB8tQcPZK5eudB7g1PHCZ5uHatbY+g==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/oidc-provider/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/oidc-provider/node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "node_modules/oidc-provider/node_modules/keyv": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.4.tgz", + "integrity": "sha512-vqNHbAc8BBsxk+7QBYLW0Y219rWcClspR6WSeoHYKG5mnsSoOH+BL1pWq02DDCVdvvuUny5rkBlzMRzoqc+GIg==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/oidc-provider/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/oidc-provider/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/oidc-provider/node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/oidc-provider/node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/oidc-provider/node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/oidc-provider/node_modules/responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "dependencies": { + "lowercase-keys": "^2.0.0" + } + }, + "node_modules/oidc-token-hash": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz", + "integrity": "sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ==", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -9112,6 +9436,31 @@ "node": ">=0.10.0" } }, + "node_modules/paseto2": { + "name": "paseto", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/paseto/-/paseto-2.1.3.tgz", + "integrity": "sha512-BNkbvr0ZFDbh3oV13QzT5jXIu8xpFc9r0o5mvWBhDU1GBkVt1IzHK1N6dcYmN7XImrUmPQ0HCUXmoe2WPo8xsg==", + "engines": { + "node": "^12.19.0 || >=14.15.0" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/paseto3": { + "name": "paseto", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/paseto/-/paseto-3.1.0.tgz", + "integrity": "sha512-oVSKoCH89M0WU3I+13NoCP9wGRel0BlQumwxsDZPk1yJtqS76PWKRM7vM9D4bz4PcScT0aIiAipC7lW6hSgkBQ==", + "optional": true, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/passthrough-counter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passthrough-counter/-/passthrough-counter-1.0.0.tgz", @@ -9619,6 +9968,17 @@ "resolved": "https://npm.lcr.gr/quick-format-unescaped/-/quick-format-unescaped-3.0.3.tgz", "integrity": "sha512-dy1yjycmn9blucmJLXOfZDx1ikZJUi6E8bBZLnhPG5gBrVhHXx2xVyqqgKBubVNEXmx51dBACMHpoMQK/N/AXQ==" }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -10041,6 +10401,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://npm.lcr.gr/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -13555,6 +13920,17 @@ "@babel/types": "^7.3.0" } }, + "@types/cacheable-request": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", + "integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==", + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -13570,6 +13946,11 @@ "@types/node": "*" } }, + "@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + }, "@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://npm.lcr.gr/@types%2fistanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -13594,6 +13975,14 @@ "@types/istanbul-lib-report": "*" } }, + "@types/keyv": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.3.tgz", + "integrity": "sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg==", + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "14.0.22", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.22.tgz", @@ -13611,6 +14000,14 @@ "integrity": "sha512-6gOkRe7OIioWAXfnO/2lFiv+SJichKVSys1mSsgyrYHSEjk8Ctv4tSR/Odvnu+HWlH2C8j53dahU03XmQdd5fA==", "dev": true }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "requires": { + "@types/node": "*" + } + }, "@types/stack-utils": { "version": "2.0.0", "resolved": "https://npm.lcr.gr/@types%2fstack-utils/-/stack-utils-2.0.0.tgz", @@ -13869,6 +14266,11 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true }, + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -14389,6 +14791,11 @@ "ylru": "^1.2.0" } }, + "cacheable-lookup": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.0.4.tgz", + "integrity": "sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A==" + }, "cacheable-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", @@ -14589,7 +14996,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "dev": true, "requires": { "mimic-response": "^1.0.0" } @@ -14954,9 +15360,9 @@ } }, "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" } @@ -15253,6 +15659,14 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "ejs": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.6.tgz", + "integrity": "sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==", + "requires": { + "jake": "^10.6.1" + } + }, "emittery": { "version": "0.7.2", "resolved": "https://npm.lcr.gr/emittery/-/emittery-0.7.2.tgz", @@ -15688,6 +16102,14 @@ "resolved": "https://npm.lcr.gr/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, + "filelist": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz", + "integrity": "sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ==", + "requires": { + "minimatch": "^3.0.4" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -16247,8 +16669,7 @@ "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" }, "http-deceiver": { "version": "1.2.7", @@ -16304,6 +16725,15 @@ "sshpk": "^1.7.0" } }, + "http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, "https-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", @@ -16759,6 +17189,17 @@ "istanbul-lib-report": "^3.0.0" } }, + "jake": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz", + "integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==", + "requires": { + "async": "0.9.x", + "chalk": "^2.4.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + } + }, "jest": { "version": "26.6.3", "resolved": "https://npm.lcr.gr/jest/-/jest-26.6.3.tgz", @@ -18504,18 +18945,18 @@ "dev": true }, "koa": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.0.tgz", - "integrity": "sha512-i/XJVOfPw7npbMv67+bOeXr3gPqOAw6uh5wFyNs3QvJ47tUx3M3V9rIE0//WytY42MKz4l/MXKyGkQ2LQTfLUQ==", + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.4.tgz", + "integrity": "sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g==", "requires": { "accepts": "^1.3.5", "cache-content-type": "^1.0.0", "content-disposition": "~0.5.2", "content-type": "^1.0.4", "cookies": "~0.8.0", - "debug": "~3.1.0", + "debug": "^4.3.2", "delegates": "^1.0.0", - "depd": "^1.1.2", + "depd": "^2.0.0", "destroy": "^1.0.4", "encodeurl": "^1.0.2", "escape-html": "^1.0.3", @@ -18524,7 +18965,7 @@ "http-errors": "^1.6.3", "is-generator-function": "^1.0.7", "koa-compose": "^4.1.0", - "koa-convert": "^1.2.0", + "koa-convert": "^2.0.0", "on-finished": "^2.3.0", "only": "~0.0.2", "parseurl": "^1.3.2", @@ -18533,18 +18974,10 @@ "vary": "^1.1.2" }, "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { + "depd": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" } } }, @@ -18582,22 +19015,12 @@ } }, "koa-convert": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", - "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", + "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", "requires": { "co": "^4.6.0", - "koa-compose": "^3.0.0" - }, - "dependencies": { - "koa-compose": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", - "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", - "requires": { - "any-promise": "^1.1.0" - } - } + "koa-compose": "^4.1.0" } }, "koa-csrf": { @@ -18918,8 +19341,7 @@ "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" }, "minimalistic-assert": { "version": "1.0.1", @@ -19026,6 +19448,11 @@ "thenify-all": "^1.0.0" } }, + "nanoid": { + "version": "3.1.30", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", + "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==" + }, "nanomatch": { "version": "1.2.13", "resolved": "https://npm.lcr.gr/nanomatch/-/nanomatch-1.2.13.tgz", @@ -19343,6 +19770,165 @@ "resolved": "https://npm.lcr.gr/obuf/-/obuf-1.1.2.tgz", "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" }, + "oidc-provider": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-7.10.1.tgz", + "integrity": "sha512-SRFVePq/ki76SbKB/vx7ach6mgc6iof2dtYOKMA88f3i1gZ6XKbqqxqyEVAj4n9L13nssUWJE9O4AyN8M0bNKA==", + "requires": { + "@koa/cors": "^3.1.0", + "cacheable-lookup": "^6.0.1", + "debug": "^4.3.2", + "ejs": "^3.1.6", + "got": "^11.8.2", + "jose": "^4.1.4", + "jsesc": "^3.0.2", + "koa": "^2.13.3", + "koa-compose": "^4.1.0", + "nanoid": "^3.1.28", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.1", + "paseto2": "npm:paseto@^2.1.3", + "paseto3": "npm:paseto@^3.0.0", + "quick-lru": "^5.1.1", + "raw-body": "^2.4.1" + }, + "dependencies": { + "@sindresorhus/is": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz", + "integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw==" + }, + "@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + } + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, + "defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "got": { + "version": "11.8.2", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", + "integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==", + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.1", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "dependencies": { + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==" + } + } + }, + "jose": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.3.6.tgz", + "integrity": "sha512-A/JgZGUerqG2IMuxkUDBtZ4aTxg/l1Y+pt/QAAYiRAR3EFlxIE0Su0xdpB8tQcPZK5eudB7g1PHCZ5uHatbY+g==" + }, + "jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==" + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "keyv": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.4.tgz", + "integrity": "sha512-vqNHbAc8BBsxk+7QBYLW0Y219rWcClspR6WSeoHYKG5mnsSoOH+BL1pWq02DDCVdvvuUny5rkBlzMRzoqc+GIg==", + "requires": { + "json-buffer": "3.0.1" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" + }, + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" + }, + "p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==" + }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "requires": { + "lowercase-keys": "^2.0.0" + } + } + } + }, + "oidc-token-hash": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz", + "integrity": "sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ==" + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -19516,6 +20102,17 @@ "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", "dev": true }, + "paseto2": { + "version": "npm:paseto@2.1.3", + "resolved": "https://registry.npmjs.org/paseto/-/paseto-2.1.3.tgz", + "integrity": "sha512-BNkbvr0ZFDbh3oV13QzT5jXIu8xpFc9r0o5mvWBhDU1GBkVt1IzHK1N6dcYmN7XImrUmPQ0HCUXmoe2WPo8xsg==" + }, + "paseto3": { + "version": "npm:paseto@3.1.0", + "resolved": "https://registry.npmjs.org/paseto/-/paseto-3.1.0.tgz", + "integrity": "sha512-oVSKoCH89M0WU3I+13NoCP9wGRel0BlQumwxsDZPk1yJtqS76PWKRM7vM9D4bz4PcScT0aIiAipC7lW6hSgkBQ==", + "optional": true + }, "passthrough-counter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passthrough-counter/-/passthrough-counter-1.0.0.tgz", @@ -19916,6 +20513,11 @@ "resolved": "https://npm.lcr.gr/quick-format-unescaped/-/quick-format-unescaped-3.0.3.tgz", "integrity": "sha512-dy1yjycmn9blucmJLXOfZDx1ikZJUi6E8bBZLnhPG5gBrVhHXx2xVyqqgKBubVNEXmx51dBACMHpoMQK/N/AXQ==" }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + }, "random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -20246,6 +20848,11 @@ "path-parse": "^1.0.6" } }, + "resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, "resolve-cwd": { "version": "3.0.0", "resolved": "https://npm.lcr.gr/resolve-cwd/-/resolve-cwd-3.0.0.tgz", diff --git a/package.json b/package.json index 126ac92971e250bee18721b0ca37c6ebaed234f9..490e5b3129ffcddcb18e624dcac90f162f1e8a7e 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "jose": "^3.6.1", "koa": "^2.13.0", "koa-bodyparser": "^4.3.0", + "koa-compose": "^4.1.0", "koa-compress": "^5.0.1", "koa-csrf": "^3.0.8", "koa-etag": "^3.0.0", @@ -55,6 +56,7 @@ "node-fetch": "^2.6.1", "nodemailer": "^6.4.17", "oauth2-server": "^3.1.1", + "oidc-provider": "^7.10.1", "pg": "^8.3.0", "pg-hstore": "^2.3.3", "pluralize": "^8.0.0", diff --git a/src/app.js b/src/app.js index 72b1476d456a34b0433ff1dfd60239dbea2b691d..98f4a76cd8219f886b8b0c8d86ba8b7525f52a1b 100644 --- a/src/app.js +++ b/src/app.js @@ -5,6 +5,9 @@ const bodyparser = require('koa-bodyparser') const etag = require('koa-etag') const session = require('koa-session') const static = require('koa-static') +const mount = require('koa-mount') + +const createOIDCServer = require('domain/auth/oidc/OIDCServer') const hbs = require('vendor/koa-handlebars') const { config } = require('bootstrap') @@ -54,15 +57,35 @@ module.exports = async function createApp(app = new Koa()) { app.use(serviceProvider.attach) - Object.values(routers).forEach(router => { + for (const [key, router] of Object.entries(routers)) { + // if (key === 'web') { + // // const OIDCService = require("./domain/auth/oidc/OIDCService"); + // // const oidcServer = await OIDCService.server + // // oidcServer.listen(7124) + // // console.log(oidcServer.urlFor('token')) + // // router.use(mount('/oidc', oidcServer.app)) + // console.log(router.routes()) + // console.log("FOO BAR BAZ") + // await ctx.services['auth.oidc'].withProvider(async prov => { + // prov.app.callback()(ctx.req, ctx.res) + // }) + // }) + // } + debug( - '[Prefix "%s"] Mounting %d layers', + '[Prefix "%s"] Mounting %d layers for %s', router.opts?.prefix ?? '/', router.stack?.length ?? 0, + key, ) + + app.use(router.routes()) app.use(router.allowedMethods()) - }) + } + + // const oidcServer = await createOIDCServer() + // app.use(oidcServer.app) return app } diff --git a/src/config/app.js b/src/config/app.js index 6cdbb052505bb40da2517c53f3d3513f682dac51..803fe3fbcb17524ee11700faf0c87c3f8ee5de34 100644 --- a/src/config/app.js +++ b/src/config/app.js @@ -11,6 +11,7 @@ module.exports = { web: env('WEB_URL', 'http://example.local'), }, dev: env('NODE_ENV', 'development') === 'development', + safe_mode: env('DISABLE_MUTATION', 'false') === 'true', security: { use_ephemeral: env('USE_EPHEMERAL_KEYS', 'true') === 'true', public_key: null, diff --git a/src/config/database.js b/src/config/database.js index c681d35091401844ce5e918c920f875e2933ea09..605593a26bdc524e56eb0618a64691c86907ea58 100644 --- a/src/config/database.js +++ b/src/config/database.js @@ -8,6 +8,7 @@ if (useSSL) { } const url = env('DATABASE_URL') + if (url) { const { URL } = require('url') const connectionUrl = new URL(url) @@ -17,6 +18,16 @@ if (url) { username: connectionUrl.username, password: connectionUrl.password, port: connectionUrl.port, + ssl: useSSL, + dialectOptions: useSSL ? { + ssl: { + rejectUnauthorized: false, + ca: Buffer.from( + env('DATABASE_CA_CERT', null) ?? '', + 'base64', + ).toString(), + }, + } : {}, log_queries: env('LOG_SQL_QUERIES', 'true') === 'true', } } else { diff --git a/src/core/errors/SafeModeError.js b/src/core/errors/SafeModeError.js new file mode 100644 index 0000000000000000000000000000000000000000..f07e5c2b83311e45c73d2f8039193cb6b9bffd97 --- /dev/null +++ b/src/core/errors/SafeModeError.js @@ -0,0 +1,7 @@ +const HttpError = require('./HttpError') + +module.exports = class SafeModeError extends HttpError { + constructor() { + super(503, `This resource is operating in "safe mode", and is exclusively accepting read-only requests`) + } +} diff --git a/src/core/injection/ServiceProvider.js b/src/core/injection/ServiceProvider.js index f0d72e7f749132847c50c76c638ec72ea24f9fec..384ead088b7645eb78b4f779e266060764120ee7 100644 --- a/src/core/injection/ServiceProvider.js +++ b/src/core/injection/ServiceProvider.js @@ -52,6 +52,7 @@ module.exports = class ServiceProvider { // require('domain/users/UserService'), require('core/services/dataloaders').DataloaderService, require('domain/auth/AuthenticationService'), + require('domain/auth/oidc/OIDCService'), require('domain/users/UserService'), require('domain/data/MetricsService'), // require('domain/eventbrite/EventbriteService'), diff --git a/src/core/utils/jwt.js b/src/core/utils/jwt.js index 13832755faa97c5a3afd81e3284765090f4f6199..53bc5074a203c5a86c0633a687ec0935a7f695d4 100644 --- a/src/core/utils/jwt.js +++ b/src/core/utils/jwt.js @@ -36,6 +36,27 @@ exports.getKeys = () => { } } +exports.getJWKS = async (type = 'pub') => { + const { config } = require('bootstrap') + const keys = exports.getKeys() + const { default: fromKeyLike } = require('jose/jwk/from_key_like') + + const key = keys[type] + const jwk = await fromKeyLike(key) + const kid = config('app.security.key_id') + + return { + keys: [ + { + kid: `${ exports.jwtOptions.keyid_prefix }${ kid }`, + use: 'sig', + ...jwk, + alg: 'RS256', + }, + ], + } +} + exports.loadKeys = async () => { const { env, config, patchConfig } = require('bootstrap') let [pub, priv] = [ @@ -107,5 +128,5 @@ exports.getClaims = tokenPayload => { exports.jwtOptions = { issuer: 'urn:jetsam:systems:auth', claims: 'urn:jetsam:resources:claims', - keyid_prefix: 'urn:jetsam:jwk:' + keyid_prefix: 'urn:jetsam:resources:jwk:' } diff --git a/src/database/models/OIDCEntity.js b/src/database/models/OIDCEntity.js new file mode 100644 index 0000000000000000000000000000000000000000..782869eab73caaa120b42085cdadae1bc4ae3203 --- /dev/null +++ b/src/database/models/OIDCEntity.js @@ -0,0 +1,86 @@ +const timestamps = require('./properties/timestamps') +const BaseModel = require('./BaseModel') + +class OIDCEntity extends BaseModel { + static associate(models) { + this.belongsTo(models.User, { foreignKey: 'related_user_id' }) + } + + toJSON() { + return { + id: this.id, + name: this.name, + description: this.description, + secret: this.secret, + redirect_uris: this.redirect_uris, + grant_types: this.grant_types, + meta: this.meta, + created_at: this.created_at, + updated_at: this.updated_at, + } + } +} + +module.exports = (sequelize, DataTypes) => { + OIDCEntity.init( + Object.assign( + { + id: { + type: DataTypes.TEXT, + primaryKey: true, + }, + related_user_id: { + type: DataTypes.UUID, + allowNull: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + references: { + model: 'users', + key: 'id', + }, + }, + grant_id: { + type: DataTypes.TEXT, + }, + user_code: { + type: DataTypes.TEXT, + }, + uid: { + type: DataTypes.TEXT, + }, + data: { + type: DataTypes.JSONB, + }, + expires_at: { + type: DataTypes.DATE, + }, + consumed_at: { + type: DataTypes.DATE, + }, + meta: { + type: DataTypes.JSONB, + defaultValue: {}, + }, + created_at: { + type: DataTypes.DATE, + validate: { + isDate: true, + }, + }, + updated_at: { + type: DataTypes.DATE, + validate: { + isDate: true, + }, + }, + } + ), + { + sequelize, + paranoid: false, + tableName: 'oidc_entities', + }, + ) + + return OIDCEntity +} diff --git a/src/database/models/User.js b/src/database/models/User.js index 25566ecf94b4e36a9dd14f99eb1a25503c5e2feb..7aa94e4010e14a00f44c48462010043a869e9d9d 100644 --- a/src/database/models/User.js +++ b/src/database/models/User.js @@ -120,15 +120,21 @@ class User extends BaseModel { async asJWTToken(extras = {}) { const { sign } = require('core/utils/jwt') - const roles = await this.getAuthRoles() return await sign({ + ...(await this.getJWTOptionsClaims()), + ...extras, + }) + } + + async getJWTOptionsClaims() { + const roles = await this.getAuthRoles() + return { [jwtOptions.claims]: { 'user-id': this.id, 'default-role': roles[0], 'allowed-roles': roles, - }, - ...extras, - }) + } + } } async getAuthRoles() { diff --git a/src/domain/auth/OAuthFlow.js b/src/domain/auth/OAuthFlow.js index 1075abf6c37578d2a9db70008346ba9f671c1a75..ce25c979f4e629ce78b374d5feb6cacd34ac6016 100644 --- a/src/domain/auth/OAuthFlow.js +++ b/src/domain/auth/OAuthFlow.js @@ -52,7 +52,18 @@ exports.showOAuthConsent = async (ctx, queryState) => { } } - const [scopes, isPrivileged] = describeScopeRequest(query.scope) + const { client, scopes, isPrivileged } = await validateOAuthState(query) + + return ctx.render('auth/accept-oauth', { + user, + client: client.toJSON(), + scopes, + redirect, + }) +} + +const validateOAuthState = exports.validateOAuthState = async function validateOAuthState(query) { + const [scopes, isPrivileged] = exports.describeScopes(query.scope) if (isPrivileged) { const allowed = new Set(config('app.security.super_auth_clients')) @@ -66,13 +77,20 @@ exports.showOAuthConsent = async (ctx, queryState) => { throw new HttpError(400, 'Invalid client id specified') } + const uri = query.redirect_uri ?? null + if (uri == null) { + throw new HttpError(400, 'Missing redirect_uri') + } - return ctx.render('auth/accept-oauth', { - user, - client: client.toJSON(), + if (!client.internal && !client.redirect_uris.includes(uri)) { + throw new HttpError(400, 'The provided redirect_uri is not authorised for this OAuth client') + } + + return { scopes, - redirect, - }) + isPrivileged, + client, + } } async function getOauthQueryFromFlow(flow) { @@ -123,7 +141,7 @@ exports.handleConsentAcceptance = async (ctx, flow, server) => { ctx.response.status = res.status } -const scopeDescriptionMap = { +const scopeDescriptionMap = exports.validScopes = { '*': { icon: 'admin', name: 'Full Access', @@ -183,10 +201,9 @@ const scopeDescriptionMap = { * @param scope * @return {[ScopeDescription[], boolean]} */ -function describeScopeRequest(scope = '*') { +exports.describeScopes = function describeScopeRequest(scope = '*') { let hasAdminRequest = false - console.log(scope) let scopes = [] if (scope.includes(',')) { scopes = scope.split(',') diff --git a/src/domain/auth/oidc/DBAdapter.js b/src/domain/auth/oidc/DBAdapter.js new file mode 100644 index 0000000000000000000000000000000000000000..baf2d7088c2491e84eb8cfc36377fad031da8930 --- /dev/null +++ b/src/domain/auth/oidc/DBAdapter.js @@ -0,0 +1,93 @@ +const moment = require('moment') +const { OIDCEntity, User, OAuthClient, AccessToken, RefreshToken, sequelize, Sequelize } = require('database/models') +const debug = require('debug')('server:auth:oidc:adapter') + +const key = (id, name) => `urn:jetsam:resources:oidc:${name}/${id}` + +class DBAdapter { + constructor(name, { model = OIDCEntity } = {}) { + this.model = model + this.name = name + } + + async upsert(id, data, expiresIn) { + // debug("INSERTING", id, data) + + const oidc = await this.model.upsert({ + id: key(id, this.name), + grant_id: data.grantId ?? null, + user_code: data.user_code ?? null, + uid: data.uid ?? null, + data, + expires_at: expiresIn ? moment().utc().add(expiresIn, 'seconds') : null, + }) + } + + async find(id) { + // debug('find', this.name, id, key(id, this.name)) + + if (typeof this[`find${ this.name }`] === 'function') { + return await this[`find${ this.name }`](id) + } + + const found = await this.model.findByPk(key(id, this.name)) + if (!found) { + return null + } + + return { + ...found.data, + ...(found.consumed_at ? { consumed: true } : {}) + } + } + + async findClient(id) { + const { OAuthClient } = require('database/models') + const client = await OAuthClient.findByPk(id) + if (client) { + return { + client_id: client.id, + client_secret: client.secret, + redirect_uris: client.redirect_uris ?? [], + grant_types: [ + 'authorization_code' + ], + response_types: ['code'] + } + } + + return null + } + + async findByUserCode(userCode) { + const found = await this.model.findOne({ where: { user_code: userCode } }); + if (!found) return undefined; + return { + ...found.data, + ...(found.consumed_at ? { consumed: true } : undefined), + }; + } + + async findByUid(uid) { + const found = await this.model.findOne({ where: { uid } }); + if (!found) return undefined; + return { + ...found.data, + ...(found.consumed_at ? { consumed: true } : undefined), + }; + } + + async destroy(id) { + await this.model.destroy({ where: { id: key(id, this.name) } }); + } + + async consume(id) { + await this.model.update({ consumed_at: new Date() }, { where: { id: key(id, this.name) } }); + } + + async revokeByGrantId(grantId) { + await this.model.destroy({ where: { grant_id: grantId } }); + } +} + +module.exports = DBAdapter \ No newline at end of file diff --git a/src/domain/auth/oidc/OIDCServer.js b/src/domain/auth/oidc/OIDCServer.js new file mode 100644 index 0000000000000000000000000000000000000000..02084f998cc69f503e19f5c8ff29737963224a82 --- /dev/null +++ b/src/domain/auth/oidc/OIDCServer.js @@ -0,0 +1,105 @@ +const { Provider } = require('oidc-provider') +const debug = require('debug')('server:auth:oidc:provider') + +module.exports = async function createOIDCServer() { + const { config } = require('bootstrap') + const DBAdapter = require("./DBAdapter"); + const { getJWKS } = require('core/utils/jwt') + const {validScopes} = require("../OAuthFlow"); + + debug(`Creating OIDC Provider with base ${config('app.host.web')}`) + const provider = new Provider( /*config('app.host.web') + 'oidc/' */ 'http://trash.4l2.uk/oidc/', { + clients: [ + { + client_id: 'kbyuFDidLLm280LIwVFiazOqjO3ty8KH', + client_secret: '60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa', + redirect_uris: ['https://openidconnect.net/callback'], + grant_types: [ + 'authorization_code', 'refresh_token' + ], + response_types: ['code'] + } + ], + scopes: Object.keys(validScopes), + jwks: await getJWKS('priv'), + adapter: DBAdapter, + cookies: { + keys: [config('app.key')], + names: { + interaction: 'jtoidc.int', + resume: 'jtoidc.res', + session: 'jtoidc.ses', + } + }, + interactions: { + url(ctx, interaction) { + debug(`OIDC Interaction`, interaction) + return `/oidc/i/${ interaction.jti }` + } + }, + renderError: async function renderError(ctx, out, error) { + console.log("ERRRR", error) + + ctx.body = `<!DOCTYPE html> + <head> + <title>oops! something went wrong</title> + <style>/* css and html classes omitted for brevity, see lib/helpers/defaults.js */</style> + </head> + <body> + <div> + <h1>oops! something went wrong</h1> + ${Object.entries(out).map(([key, value]) => `<pre><strong>${key}</strong>: ${value}</pre>`).join('')} + </div> + </body> + </html>` + + ctx.type = 'text/html' + }, + features: { + devInteractions: { + enabled: false, + }, + }, + pkce: { + required: () => false, + }, + routes: { + authorization: '/oidc/auth', + backchannel_authentication: '/oidc/backchannel', + code_verification: '/oidc/device', + device_authorization: '/oidc/device/oidc/auth', + end_session: '/oidc/session/oidc/end', + introspection: '/oidc/token/oidc/introspection', + jwks: '/.well-known/jwks.json', + pushed_authorization_request: '/oidc/request', + registration: '/oidc/reg', + revocation: '/oidc/token/oidc/revocation', + token: '/oidc/token', + userinfo: '/oidc/me' + }, + async findAccount(ctx, id) { + debug("find account", id) + return { + accountId: id, + async claims(use, scope) { + const { User } = require('database/models') + const user = await User.findByPk(id) + debug(`Finding account for OIDC ${id}, ${use}, ${scope}`) + const details = await user.getJWTOptionsClaims() + + console.log(details) + + return { + sub: user.id, + id: user.id, + email: user.email, + scope, + ...details + } + } + } + } + }) + + return provider +} \ No newline at end of file diff --git a/src/domain/auth/oidc/OIDCService.js b/src/domain/auth/oidc/OIDCService.js new file mode 100644 index 0000000000000000000000000000000000000000..eb26d6373de656175482fcac4bd849bae2ac7532 --- /dev/null +++ b/src/domain/auth/oidc/OIDCService.js @@ -0,0 +1,25 @@ +const ContextualModule = require('core/injection/ContextualModule') +const createOIDCServer = require('domain/auth/oidc/OIDCServer') + +module.exports = class OIDCService extends ContextualModule { + static getServiceName() { + return 'auth.oidc' + } + + static get profileMethods() { + return ['withProvider', 'getInteraction'] + } + + async init() { + this.provider = await provider + } + + async withProvider(fn) { + return await fn(this.provider) + } + + async getInteraction() { + return await this.provider.interactionDetails(this.ctx.req, this.ctx.res) + } +} +let provider = module.exports.server = createOIDCServer() \ No newline at end of file diff --git a/src/http/controllers/auth.js b/src/http/controllers/auth.js index 1284e3b269e41f6d0745c92c0a958b705642467f..fe8927530bc5916e61aaf0f5baea715cc1c9afbe 100644 --- a/src/http/controllers/auth.js +++ b/src/http/controllers/auth.js @@ -24,6 +24,7 @@ exports.showLogin = async ctx => { } data.state_string = `?${ state.toString() }` + data.login_action = `/login${ data.state_string}` return ctx.render('auth/login', data) } diff --git a/src/http/controllers/oidc.js b/src/http/controllers/oidc.js new file mode 100644 index 0000000000000000000000000000000000000000..d8d6a9794ab8c971b82d6e635a6002b6d18bd2bf --- /dev/null +++ b/src/http/controllers/oidc.js @@ -0,0 +1,114 @@ +const compose = require('koa-compose') +const HttpError = require("../../core/errors/HttpError"); +const {describeScopes, validateOAuthState} = require("../../domain/auth/OAuthFlow"); +const debug = require('debug')('server:routes:oidc') + +exports.mapRoutes = async ctx => { + await ctx.services['auth.oidc'].withProvider(async prov => { + const middleware = prov.app.middleware + const fn = compose(middleware) + await prov.app.handleRequest(ctx, fn) + }) +} + +exports.handleLogin = async ctx => { + const interaction = await ctx.services['auth.oidc'].getInteraction() + const { uid, prompt, params, session } = interaction + + if (prompt.name !== 'login') { + throw new HttpError(400, 'Invalid interaction prompt') + } + + const { email, password } = ctx.request.body + const user = await ctx.services['core.auth'].attemptLogin(email, password) + + const result = { + login: { + accountId: user.id, + }, + } + + return await ctx.services['auth.oidc'].withProvider(p => p.interactionFinished(ctx.req, ctx.res, result, { + mergeWithLastSubmission: false, + })) +} + +exports.interaction = async ctx => { + const interaction = await ctx.services['auth.oidc'].getInteraction() + const { uid, prompt, params, session } = interaction + + debug("Interaction handler", interaction) + + switch (prompt.name) { + case 'login': + return await ctx.render('auth/login', { + login_action: `/oidc/i/${uid}/login` + }) + case 'consent': + const { client, scopes } = await validateOAuthState(params) + return await ctx.render('auth/accept-oauth', { + client: client.toJSON(), + scopes, + confirm_action: `/oidc/i/${ uid }/confirm`, + reject_action: `/oidc/i/${ uid }/reject`, + }) + } +} + +exports.confirm = async ctx => { + const interaction = await ctx.services['auth.oidc'].getInteraction() + const { uid, prompt, params, session, ...details } = interaction + + if (prompt.name !== 'consent') { + throw new HttpError(400, 'Invalid interaction prompt') + } + + let grantId = details.grantId + const grant = await ctx.services['auth.oidc'].withProvider(provider => { + if (grantId) { + return provider.Grant.find(grantId); + } else { + return new provider.Grant({ + accountId: session.accountId, + clientId: params.client_id, + jti: details.jti ?? undefined, + }); + } + }) + + if (prompt.details.missingOIDCScope) { + grant.addOIDCScope(prompt.details.missingOIDCScope.join(' ')); + } + if (prompt.details.missingOIDCClaims) { + grant.addOIDCClaims(prompt.details.missingOIDCClaims); + } + if (prompt.details.missingResourceScopes) { + // eslint-disable-next-line no-restricted-syntax + for (const [indicator, scope] of Object.entries(prompt.details.missingResourceScopes)) { + grant.addResourceScope(indicator, scope.join(' ')); + } + } + + grantId = await grant.save() + + const consent = {} + if (!details.grantId) { + consent.grantId = grantId + } + const result = { consent } + + return await ctx.services['auth.oidc'].withProvider(p => p.interactionFinished(ctx.req, ctx.res, result, { + mergeWithLastSubmission: true, + })) +} + +exports.reject = async ctx => { + const result = { + error: 'access_denied', + error_description: 'The request for access was rejected', + } + + return await ctx.services['auth.oidc'].withProvider(p => p.interactionFinished(ctx.req, ctx.res, result, { + mergeWithLastSubmission: false, + })) +} \ No newline at end of file diff --git a/src/http/middleware/ErrorHandler.js b/src/http/middleware/ErrorHandler.js index 86b9e163a2771d163dc5ab536d81821e3875ce5b..29985984bc80c85b4a333e9c38f1e3c69e8d3cea 100644 --- a/src/http/middleware/ErrorHandler.js +++ b/src/http/middleware/ErrorHandler.js @@ -1,4 +1,5 @@ const HttpError = require('core/errors/HttpError') +const SafeModeError = require('core/errors/SafeModeError') const SentryReporter = require('./SentryReporter') module.exports = async (ctx, next) => { @@ -7,7 +8,11 @@ module.exports = async (ctx, next) => { try { await next(ctx) } catch (e) { - await SentryReporter.report(e, ctx) + if (e instanceof SafeModeError) { + console.error(e) + } else { + await SentryReporter.report(e, ctx) + } hasHandledError = true if (e instanceof HttpError) { diff --git a/src/http/middleware/SafeModeBlock.js b/src/http/middleware/SafeModeBlock.js new file mode 100644 index 0000000000000000000000000000000000000000..f90ecd9ac1807b811ae82bfeb226ec575db115f2 --- /dev/null +++ b/src/http/middleware/SafeModeBlock.js @@ -0,0 +1,10 @@ +const SafeModeError = require('core/errors/SafeModeError') + +module.exports = async (ctx, next) => { + const { config } = require('bootstrap') + const safeMode = config('app.safe_mode') + if (safeMode) { + throw new SafeModeError() + } + return await next() +} diff --git a/src/http/routers/routes_v2.js b/src/http/routers/routes_v2.js index 8f9ea777a90c90e011b6e8be169892d069615d0f..45691f1663a664f4481155ab33d3614e2eec492e 100644 --- a/src/http/routers/routes_v2.js +++ b/src/http/routers/routes_v2.js @@ -5,6 +5,7 @@ const router = new Router({ prefix: '/v2' }) const controller = (path, handler) => require(`../controllers/${path}`)[handler] const param = name => require(`../params/${name}`) const { env, config } = require('bootstrap') +const safemode = require('http/middleware/SafeModeBlock') apiMiddlewareGroup.forEach(middleware => router.use(middleware)) @@ -25,27 +26,28 @@ router.get( }), ) -router.post('/auth/login', controller('api/auth', 'login')) -router.post('/auth/register', controller('api/auth', 'register')) +router.post('/auth/login', safemode, controller('api/auth', 'login')) +router.post('/auth/register', safemode, controller('api/auth', 'register')) router.post( '/auth/password-reset', + safemode, controller('api/auth', 'triggerPasswordReset'), ) router.get('/self', controller('api/user', 'self')) router.get('/self/surveys', controller('api/v2/surveys', 'joined')) router.get('/self/bundles', controller('api/app', 'getBundles')) -router.put('/self/:property', controller('api/user', 'updateOne')) +router.put('/self/:property', safemode, controller('api/user', 'updateOne')) router.get('/metrics', controller('api/content', 'getWithin')) -router.post('/metrics', controller('api/content', 'postMetric')) +router.post('/metrics', safemode, controller('api/content', 'postMetric')) router.get('/images', noop) router.post('/images', noop) router.post('/images/:imageId/share', noop) router.get('/uploads', noop) -router.post('/uploads', controller('api/v2/uploads', 'createUpload')) +router.post('/uploads', safemode, controller('api/v2/uploads', 'createUpload')) router.get('/uploads/:upload_id', noop) router.delete('/uploads/:upload_id', noop) router.put('/uploads/:upload_id/:property', noop) @@ -57,12 +59,12 @@ router.get('/surveys', controller('api/v2/surveys', 'list')) router.get('/excerpts', controller('api/v2/surveys', 'listExcerpts')) router.get('/excerpts/:excerpt', controller('api/v2/surveys', 'getExcerpt')) router.get('/surveys/:survey', controller('api/v2/surveys', 'get')) -router.post('/surveys/:survey/membership', controller('api/v2/surveys', 'join')) -router.delete('/surveys/:survey/membership', controller('api/v2/surveys', 'leave')) +router.post('/surveys/:survey/membership', safemode, controller('api/v2/surveys', 'join')) +router.delete('/surveys/:survey/membership', safemode, controller('api/v2/surveys', 'leave')) if (config('app.dev')) { - router.post('/surveys/factory', controller('api/v2/factories', 'survey')) + router.post('/surveys/factory', safemode, controller('api/v2/factories', 'survey')) } -router.post('/an/ev', controller('api/analytics', 'track')) +router.post('/an/ev', safemode, controller('api/analytics', 'track')) module.exports = router diff --git a/src/http/routes.js b/src/http/routes.js index f268250e40587a65367a59ed96462775360e14dd..c948e3ba9ec693d6a0d075dfffce30e7fecc657b 100644 --- a/src/http/routes.js +++ b/src/http/routes.js @@ -17,37 +17,42 @@ const loaders = require('http/middleware/MountLoaders') const userGate = require('http/middleware/RequiresAuth') const authRedirect = require('http/middleware/RedirectToLogin') const device = require('http/middleware/DeviceProperties').extractDevice +const safemode = require('http/middleware/SafeModeBlock') + +const createOIDCServer = require('domain/auth/oidc/OIDCServer') const v2 = require('./routers/routes_v2') const well_known = new Router({ prefix: '/.well-known' }) well_known.get('wk.jwks', '/jwks.json', async ctx => { - const { getKeys } = require('core/utils/jwt') - const { pub } = getKeys() - const { default: fromKeyLike } = require('jose/jwk/from_key_like') - - const jwk = await fromKeyLike(pub) - + const { getJWKS } = require('core/utils/jwt') ctx.set('Cache-Control', `public, max-age=30`) - ctx.body = { - keys: [ - { - use: 'sig', - ...jwk, - alg: 'RS256', - }, - ], - } + ctx.body = await getJWKS() }) +well_known.get('wk.oidc', '/openid-configuration', controller('oidc', 'mapRoutes')) const web = new Router() web.use(profiling) web.use(device) +web.all('/test/oidc', ctx => { + ctx.body = { + body: ctx.request.body, + query: ctx.request.query, + headers: ctx.request.headers, + } +}) + web.use(well_known.allowedMethods()) web.use(well_known.routes()) +web.all('/oidc/i/:uid', controller('oidc', 'interaction')) +web.all('/oidc/i/:uid/login', controller('oidc', 'handleLogin')) +web.all('/oidc/i/:uid/confirm', controller('oidc', 'confirm')) +web.all('/oidc/i/:uid/reject', controller('oidc', 'reject')) +web.all(/^\/oidc\/.*/, controller('oidc', 'mapRoutes')) + web.get('/login', controller('auth', 'showLogin')) web.post('/login', controller('auth', 'login')) web.get('/logout', controller('auth', 'logout')) @@ -99,61 +104,67 @@ function mount(api) { } }) - api.post('/metrics', controller('api/content', 'postMetric')) + api.post('/metrics', safemode, controller('api/content', 'postMetric')) api.get('/metrics', controller('api/content', 'getWithin')) api.get('/images', controller('api/storage', 'getFiles')) api.post( '/images', + safemode, upload.single('featured_image'), controller('api/storage', 'saveFile'), ) api.post( '/images/:imageId/feature', + safemode, controller('api/storage', 'featureImage'), ) /** @deprecated */ api.post( '/feature', + safemode, upload.single('featured_image'), controller('api/storage', 'saveFile'), ) api.get('/feed', controller('api/storage', 'feed')) - api.post('/feed/:fileId/like', controller('api/storage', 'like')) - api.post('/feed/:fileId/unlike', controller('api/storage', 'unlike')) + api.post('/feed/:fileId/like', safemode, controller('api/storage', 'like')) + api.post('/feed/:fileId/unlike', safemode, controller('api/storage', 'unlike')) - api.post('/register', controller('api/auth', 'register')) + api.post('/register', safemode, controller('api/auth', 'register')) api.post('/login', controller('api/auth', 'login')) - api.post('/auth/reset-token', controller('api/auth', 'triggerPasswordReset')) + api.post('/auth/reset-token', safemode, controller('api/auth', 'triggerPasswordReset')) api.post( '/auth/reset-password', + safemode, controller('api/auth', 'handlePasswordReset'), ) api.param('oauthClientId', param('oauth_client')) api.get('/oauth/clients', controller('api/oauth', 'listClients')) - api.post('/oauth/clients', controller('api/oauth', 'createClient')) + api.post('/oauth/clients', safemode, controller('api/oauth', 'createClient')) api.post( '/oauth/clients/:oauthClientId/redirects', + safemode, controller('api/oauth', 'addClientRedirect'), ) api.delete( '/oauth/clients/:oauthClientId/redirects', + safemode, controller('api/oauth', 'removeClientRedirect'), ) api.get('/self', controller('api/user', 'self')) api.get('/self/bundles', controller('api/app', 'getBundles')) - api.put('/self/:property', controller('api/user', 'updateOne')) + api.put('/self/:property', safemode, controller('api/user', 'updateOne')) api.post('/an/id', async ctx => {}) - api.post('/an/ev', controller('api/analytics', 'track')) + api.post('/an/ev', safemode, controller('api/analytics', 'track')) - api.post('/feedback', controller('api/feedback', 'send')) + api.post('/feedback', safemode, controller('api/feedback', 'send')) api.use(v2.allowedMethods()) api.use(v2.routes()) diff --git a/views/auth/accept-oauth.hbs b/views/auth/accept-oauth.hbs index 7b1433a8d91cb71cc65b70ae4ec36144187c2563..4de72588e7cc77d22448d3f51fec2f0f4d24aceb 100644 --- a/views/auth/accept-oauth.hbs +++ b/views/auth/accept-oauth.hbs @@ -36,14 +36,26 @@ {{/each}} </ul> <div class="row space-4 center-content"> - <form method="post" - action="/auth/authorize?action=accept&auth_state={{redirect}}"> + <form + method="post" + {{#if confirm_action}} + action="{{confirm_action}}" + {{else}} + action="/auth/authorize?action=accept&auth_state={{redirect}}" + {{/if}} + > <button type="submit" class="button success">Accept </button> </form> - <form method="post" - action="/auth/authorize?action=deny&auth_state={{redirect}}"> + <form + method="post" + {{#if reject_action}} + action="{{reject_action}}" + {{else}} + action="/auth/authorize?action=deny&auth_state={{redirect}}" + {{/if}} + > <button type="submit" class="button danger"> Reject </button> diff --git a/views/auth/login.hbs b/views/auth/login.hbs index e2d1145923d3fe7ea8eee25470e84ac2601926be..3cd34540583ccbda20f70f9c1b39e391ac760aca 100644 --- a/views/auth/login.hbs +++ b/views/auth/login.hbs @@ -34,22 +34,26 @@ </p> </div> - <form class="form column space-1" method="post" action="/login{{#if state_string}}{{state_string}}{{/if}}"> - <div class="control column"> - <label for="email">Email Address</label> - <input id="email" name="email" type="email" placeholder="Email Address"/> - </div> - <div class="control column"> - <label for="password">Password</label> - <input id="password" name="password" type="password" placeholder="Password"/> - </div> + {{#if login_action}} + <form class="form column space-1" method="post" action="{{login_action}}"> + {{else}} + <form class="form column space-1" method="post" action="/login{{#if state_string}}{{state_string}}{{/if}}"> + {{/if}} + <div class="control column"> + <label for="email">Email Address</label> + <input id="email" name="email" type="email" placeholder="Email Address"/> + </div> + <div class="control column"> + <label for="password">Password</label> + <input id="password" name="password" type="password" placeholder="Password"/> + </div> - <button type="submit" class="button primary"> - Log In - </button> - <div class="row center-content"> - <a href="/reset-password">Forgotten Password?</a> - </div> + <button type="submit" class="button primary"> + Log In + </button> + <div class="row center-content"> + <a href="/reset-password">Forgotten Password?</a> + </div> </form> </div> <div class="split-child bg-jetsam center-content column space-2">