diff --git a/.gitignore b/.gitignore index 1c4d01727..9a0032d95 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ images/app-icons/mac/*.png /playwright-report/ /blob-report/ /playwright/.cache/ -/playwright/.auth/ \ No newline at end of file +/playwright/.auth/ + +.env \ No newline at end of file diff --git a/README.md b/README.md index 1800f667e..f57164ada 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Feel free to join our official conversations. We would love to hear what feature * Fast and easy [navigation between notes](https://triliumnext.github.io/Docs/Wiki/note-navigation), full text search and [note hoisting](https://triliumnext.github.io/Docs/Wiki/note-hoisting) * Seamless [note versioning](https://triliumnext.github.io/Docs/Wiki/note-revisions) * Note [attributes](https://triliumnext.github.io/Docs/Wiki/attributes) can be used for note organization, querying and advanced [scripting](https://triliumnext.github.io/Docs/Wiki/scripts) +* Direct OpenID and TOTP integration for more secure login * [Synchronization](https://triliumnext.github.io/Docs/Wiki/synchronization) with self-hosted sync server * there's a [3rd party service for hosting synchronisation server](https://trilium.cc/paid-hosting) * [Sharing](https://triliumnext.github.io/Docs/Wiki/sharing) (publishing) notes to public internet diff --git a/db/schema.sql b/db/schema.sql index 1b4c46321..2f6a18ef1 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -126,6 +126,19 @@ CREATE TABLE IF NOT EXISTS "attachments" utcDateScheduledForErasureSince TEXT DEFAULT NULL, isDeleted INT not null, deleteId TEXT DEFAULT NULL); +CREATE TABLE IF NOT EXISTS "user_data" +( + tmpID INT, + username TEXT, + email TEXT, + userIDEcnryptedDataKey TEXT, + userIDVerificationHash TEXT, + salt TEXT, + derivedKey TEXT, + isSetup TEXT DEFAULT "false", + UNIQUE (tmpID), + PRIMARY KEY (tmpID) +); CREATE INDEX IDX_attachments_ownerId_role on attachments (ownerId, role); diff --git a/example.env b/example.env new file mode 100644 index 000000000..48c81fb7c --- /dev/null +++ b/example.env @@ -0,0 +1,7 @@ +SSO_ENABLED="false" +BASE_URL="http://localhost:8080" +CLIENT_ID="1234" +SECRET="I-Like-Trilium-Notes" + +TOTP_ENABLED="false" +TOTP_SECRET="Trilium-Notes-is-the-best" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 990dab0c2..ab62b64da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "dayjs": "1.11.13", "dayjs-plugin-utc": "0.1.2", "debounce": "2.2.0", + "dotenv": "^16.4.5", "ejs": "3.1.10", "electron-debug": "4.1.0", "electron-dl": "4.0.0", @@ -41,6 +42,7 @@ "escape-html": "1.0.3", "eslint": "9.17.0", "express": "4.21.2", + "express-openid-connect": "^2.17.1", "express-rate-limit": "7.5.0", "express-session": "1.18.1", "force-graph": "1.47.1", @@ -91,6 +93,7 @@ "split.js": "1.6.5", "stream-throttle": "0.1.3", "striptags": "3.2.0", + "time2fa": "^1.3.0", "tmp": "0.2.3", "tree-kill": "1.2.2", "ts-loader": "9.5.1", @@ -1420,278 +1423,6 @@ "node": ">=14.14" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", @@ -1709,125 +1440,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -1975,6 +1587,21 @@ "@shikijs/vscode-textmate": "^9.3.1" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@highlightjs/cdn-assets": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@highlightjs/cdn-assets/-/cdn-assets-11.11.0.tgz", @@ -3589,6 +3216,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3655,11 +3291,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -3672,7 +3328,6 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, "license": "MIT", "dependencies": { "defer-to-connect": "^2.0.0" @@ -3796,7 +3451,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-cache-semantics": "*", @@ -4260,7 +3914,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "dev": true, "license": "MIT" }, "node_modules/@types/http-errors": { @@ -4326,7 +3979,6 @@ "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -4393,7 +4045,6 @@ "version": "22.10.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -4427,7 +4078,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -4969,7 +4619,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, "license": "MIT", "dependencies": { "clean-stack": "^2.0.0", @@ -5119,44 +4768,6 @@ "node": ">= 8" } }, - "node_modules/appdmg": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/appdmg/-/appdmg-0.6.6.tgz", - "integrity": "sha512-GRmFKlCG+PWbcYF4LUNonTYmy0GjguDy6Jh9WP8mpd0T6j80XIJyXBiWlD0U+MLNhqV9Nhx49Gl9GpVToulpLg==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "async": "^1.4.2", - "ds-store": "^0.1.5", - "execa": "^1.0.0", - "fs-temp": "^1.0.0", - "fs-xattr": "^0.3.0", - "image-size": "^0.7.4", - "is-my-json-valid": "^2.20.0", - "minimist": "^1.1.3", - "parse-color": "^1.0.0", - "path-exists": "^4.0.0", - "repeat-string": "^1.5.4" - }, - "bin": { - "appdmg": "bin/appdmg.js" - }, - "engines": { - "node": ">=8.5" - } - }, - "node_modules/appdmg/node_modules/async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -5495,17 +5106,6 @@ "license": "Apache-2.0", "optional": true }, - "node_modules/base32-encode": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/base32-encode/-/base32-encode-1.2.0.tgz", - "integrity": "sha512-cHFU8XeRyx0GgmoWi5qHMCVRiqU6J3MHWxVgun7jggCBUpVzm1Ir7M9dYr2whjSNc3tFeXfQ/oZjQu/4u55h9A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "to-data-view": "^1.1.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -5526,6 +5126,15 @@ ], "license": "MIT" }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -5798,17 +5407,6 @@ "object-assign": "^4.1.1" } }, - "node_modules/bplist-creator": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.8.tgz", - "integrity": "sha512-Za9JKzD6fjLC16oX2wsXfc+qBEhJBJB1YPInoAQpMLhDuj5aVOv1baGeIQSq1Fr3OCqzvsoQcSBSwGId/Ja2PA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "stream-buffers": "~2.2.0" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -6112,7 +5710,6 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.6.0" @@ -6122,7 +5719,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dev": true, "license": "MIT", "dependencies": { "clone-response": "^1.0.2", @@ -6141,7 +5737,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, "license": "MIT", "dependencies": { "pump": "^3.0.0" @@ -6403,7 +5998,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6585,7 +6179,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^1.0.0" @@ -7787,7 +7380,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -8019,17 +7611,16 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/ds-store": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/ds-store/-/ds-store-0.1.6.tgz", - "integrity": "sha512-kY21M6Lz+76OS3bnCzjdsJSF7LBpLYGCVfavW8TgQD2XkcqIZ86W0y9qUDZu6fp7SIZzqosMDW2zi7zVFfv4hw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bplist-creator": "~0.0.3", - "macos-alias": "~0.2.5", - "tn1150": "^0.1.0" + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/dunder-proto": { @@ -8982,14 +8573,6 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, - "node_modules/encode-utf8": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", - "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -9587,11 +9170,97 @@ "vary": "~1.1.2" }, "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-openid-connect": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/express-openid-connect/-/express-openid-connect-2.17.1.tgz", + "integrity": "sha512-5pVK6PNV09x6UN29R9Mer0XF3hwQq2HxiFsjZvLuIQ9ezeTUGbqrefzBOpzciz1S/1WWVaVPDIcj4EBpD8WB3Q==", + "license": "MIT", + "dependencies": { + "base64url": "^3.0.1", + "clone": "^2.1.2", + "cookie": "^0.5.0", + "debug": "^4.3.4", + "futoin-hkdf": "^1.5.1", + "http-errors": "^1.8.1", + "joi": "^17.7.0", + "jose": "^2.0.6", + "on-headers": "^1.0.2", + "openid-client": "^4.9.1", + "url-join": "^4.0.1" + }, + "engines": { + "node": "^10.19.0 || >=12.0.0 < 13 || >=13.7.0 < 14 || >= 14.2.0" + }, + "peerDependencies": { + "express": ">= 4.17.0" + } + }, + "node_modules/express-openid-connect/node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/express-openid-connect/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-openid-connect/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-openid-connect/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-openid-connect/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-openid-connect/node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" } }, "node_modules/express-rate-limit": { @@ -10146,17 +9815,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/fmix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/fmix/-/fmix-0.1.0.tgz", - "integrity": "sha512-Y6hyofImk9JdzU8k5INtTXX1cu8LDlePWDFU5sftm9H+zKCr5SGrVjdhkvsim646cw5zD0nADj8oHyXMZmCZ9w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "imul": "^1.0.0" - } - }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -10316,32 +9974,6 @@ "node": ">= 8" } }, - "node_modules/fs-temp": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/fs-temp/-/fs-temp-1.2.1.tgz", - "integrity": "sha512-okTwLB7/Qsq82G6iN5zZJFsOfZtx2/pqrA7Hk/9fvy+c+eJS9CvgGXT2uNxwnI14BDY9L/jQPkaBgSvlKfSW9w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "random-path": "^0.1.0" - } - }, - "node_modules/fs-xattr": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/fs-xattr/-/fs-xattr-0.3.1.tgz", - "integrity": "sha512-UVqkrEW0GfDabw4C3HOrFlxKfx0eeigfRne69FxSBdHIP8Qt5Sq6Pu3RM9KmMlkygtC4pPKkj5CiPO5USnj2GA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "!win32" - ], - "engines": { - "node": ">=8.6.0" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -10349,21 +9981,6 @@ "dev": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -10373,6 +9990,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/futoin-hkdf": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.3.tgz", + "integrity": "sha512-SewY5KdMpaoCeh7jachEWFsh1nNlaDjNHZXWqL5IGwtpEYHTgkr2+AMCgNwKWkcc0wpSYrZfR7he4WdmHFtDxQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/galactus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/galactus/-/galactus-1.0.0.tgz", @@ -10501,28 +10127,6 @@ "node": ">=8" } }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-property": "^1.0.2" - } - }, - "node_modules/generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha512-TuOwZWgJ2VAMEGJvAyPWvpqxSANF0LDpmyHauMjFYzaACvn+QTT/AZomvPCzVBV7yDN3OmwHQ5OvHaeLKre3JQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-property": "^1.0.0" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -10835,7 +10439,6 @@ "version": "11.8.6", "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, "license": "MIT", "dependencies": { "@sindresorhus/is": "^4.0.0", @@ -11201,7 +10804,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true, "license": "BSD-2-Clause" }, "node_modules/http-errors": { @@ -11276,7 +10878,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", @@ -11516,20 +11117,6 @@ "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", "license": "MIT" }, - "node_modules/image-size": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.7.5.tgz", - "integrity": "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==", - "dev": true, - "license": "MIT", - "optional": true, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/image-type": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/image-type/-/image-type-5.2.0.tgz", @@ -11587,17 +11174,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/imul": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/imul/-/imul-1.0.1.tgz", - "integrity": "sha512-WFAgfwPLAjU66EKt6vRdTlKj4nAgIDQzh29JonLa4Bqtl6D8JrIMvWjCnx7xEjVNmP3U0fM5o8ZObk7d0f62bA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -11611,7 +11187,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -11836,29 +11411,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-my-ip-valid": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.1.tgz", - "integrity": "sha512-jxc8cBcOWbNK2i2aTkCZP6i7wkHF1bqKFrwEHuN5Jtg5BSaZHUZQ/JTOJwoV41YvHnOaRyWWh72T/KvfNz9DJg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/is-my-json-valid": { - "version": "2.20.6", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz", - "integrity": "sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "generate-function": "^2.0.0", - "generate-object-property": "^1.1.0", - "is-my-ip-valid": "^1.0.0", - "jsonpointer": "^5.0.0", - "xtend": "^4.0.0" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -11892,14 +11444,6 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "license": "MIT" }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -12190,12 +11734,40 @@ "node": ">=18" } }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/joplin-turndown-plugin-gfm": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/joplin-turndown-plugin-gfm/-/joplin-turndown-plugin-gfm-1.0.12.tgz", "integrity": "sha512-qL4+1iycQjZ1fs8zk3jSRk7cg3ROBUHk7GKtiLAQLFzLPKErnILUvz5DLszSQvz3s1sTjPbywLDISVUtBY6HaA==", "license": "MIT" }, + "node_modules/jose": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.7.tgz", + "integrity": "sha512-5hFWIigKqC+e/lRyQhfnirrAqUdIPMB7SJRqflJaO29dW7q5DFvH1XCSTmv6PQ6pb++0k6MJlLRoS0Wv4s38Wg==", + "license": "MIT", + "dependencies": { + "@panva/asn1.js": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0 < 13 || >=13.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/jpeg-js": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", @@ -12410,17 +11982,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsonpointer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/jsplumb": { "version": "2.15.6", "resolved": "https://registry.npmjs.org/jsplumb/-/jsplumb-2.15.6.tgz", @@ -12884,7 +12445,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12941,26 +12501,10 @@ "node": ">= 6" } }, - "node_modules/macos-alias": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/macos-alias/-/macos-alias-0.2.12.tgz", - "integrity": "sha512-yiLHa7cfJcGRFq4FrR4tMlpNHb4Vy4mWnpajlSSIFM5k4Lv8/7BbbDLzCAVogWNl0LlLhizRp1drXv0hK9h0Yw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "nan": "^2.4.0" - } - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, "license": "ISC" }, "node_modules/make-fetch-happen": { @@ -13108,9 +12652,9 @@ } }, "node_modules/math-intrinsics": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz", - "integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -13311,7 +12855,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, "license": "MIT", "engines": { "node": ">=4" @@ -13510,27 +13053,6 @@ "node": ">= 6.0.0" } }, - "node_modules/murmur-32": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/murmur-32/-/murmur-32-0.2.0.tgz", - "integrity": "sha512-ZkcWZudylwF+ir3Ld1n7gL6bI2mQAzXvSobPwVtu8aYi2sbXeipeSkdcanRLzIofLcM5F53lGaKm2dk7orBi7Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "encode-utf8": "^1.0.3", - "fmix": "^0.1.0", - "imul": "^1.0.0" - } - }, - "node_modules/nan": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", - "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -13884,7 +13406,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -13975,6 +13496,15 @@ "node": ">=0.10.0" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", @@ -13998,6 +13528,15 @@ "node": ">= 0.4" } }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/omggif": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", @@ -14059,6 +13598,39 @@ "opencollective-postinstall": "index.js" } }, + "node_modules/openid-client": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-4.9.1.tgz", + "integrity": "sha512-DYUF07AHjI3QDKqKbn2F7RqozT4hyi4JvmpodLrq0HHoNP7t/AjeG/uqiBK1/N2PZSAQEThVjDLHSmJN4iqu/w==", + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.1.0", + "got": "^11.8.0", + "jose": "^2.0.5", + "lru-cache": "^6.0.0", + "make-error": "^1.3.6", + "object-hash": "^2.0.1", + "oidc-token-hash": "^5.0.1" + }, + "engines": { + "node": "^10.19.0 || >=12.0.0 < 13 || >=13.7.0 < 14 || >= 14.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -14154,7 +13726,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14344,24 +13915,6 @@ "node": ">=4.0" } }, - "node_modules/parse-color": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-color/-/parse-color-1.0.0.tgz", - "integrity": "sha512-fuDHYgFHJGbpGMgw9skY/bj3HL/Jrn4l/5rSspy00DoT4RyLnDcRvPxdZ+r6OFwIsgAuhDh4I09tAId4mI12bw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "~0.5.0" - } - }, - "node_modules/parse-color/node_modules/color-convert": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", - "integrity": "sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling==", - "dev": true, - "optional": true - }, "node_modules/parse-headers": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", @@ -15091,7 +14644,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -15118,18 +14670,6 @@ "node": ">= 0.8" } }, - "node_modules/random-path": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/random-path/-/random-path-0.1.2.tgz", - "integrity": "sha512-4jY0yoEaQ5v9StCl5kZbNIQlg1QheIDBrdkDn53EynpPb9FgO6//p3X/tgMnrC45XN6QZCzU1Xz/+pSSsJBpRw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "base32-encode": "^0.1.0 || ^1.0.0", - "murmur-32": "^0.1.0 || ^0.2.0" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -15586,17 +15126,6 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "license": "MIT" }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10" - } - }, "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -15745,7 +15274,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true, "license": "MIT" }, "node_modules/resolve-cwd": { @@ -15828,7 +15356,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, "license": "MIT", "dependencies": { "lowercase-keys": "^2.0.0" @@ -16858,17 +16385,6 @@ "node": ">= 0.8" } }, - "node_modules/stream-buffers": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", - "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", - "dev": true, - "license": "Unlicense", - "optional": true, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/stream-throttle": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", @@ -17459,6 +16975,12 @@ "tslib": "^2" } }, + "node_modules/time2fa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/time2fa/-/time2fa-1.4.2.tgz", + "integrity": "sha512-badZQkQpCi8eZWN02HTjoBBg+leBmwiLWFQtweklEhY8+JEGXSgd2Xy6nGBtPi+7HigSczclYTljAEJA4Z9D4g==", + "license": "MIT" + }, "node_modules/timm": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz", @@ -17536,28 +17058,6 @@ "tmp": "^0.2.0" } }, - "node_modules/tn1150": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/tn1150/-/tn1150-0.1.0.tgz", - "integrity": "sha512-DbplOfQFkqG5IHcDyyrs/lkvSr3mPUVsFf/RbDppOshs22yTPnSJWEe6FkYd1txAwU/zcnR905ar2fi4kwF29w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "unorm": "^1.4.1" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/to-data-view": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-1.1.0.tgz", - "integrity": "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -17805,21 +17305,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/tsx/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -18015,7 +17500,6 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, "license": "MIT" }, "node_modules/unescape": { @@ -18065,17 +17549,6 @@ "node": ">= 10.0.0" } }, - "node_modules/unorm": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", - "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==", - "dev": true, - "license": "MIT or GPL-2.0", - "optional": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -18162,6 +17635,12 @@ "punycode": "^2.1.0" } }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "license": "MIT" + }, "node_modules/username": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/username/-/username-5.1.0.tgz", @@ -18989,7 +18468,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC" }, "node_modules/yaml": { diff --git a/package.json b/package.json index 61d85cc78..f6040652e 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,8 @@ "csurf": "1.11.0", "dayjs": "1.11.13", "dayjs-plugin-utc": "0.1.2", + "dotenv": "^16.4.5", + "express-openid-connect": "^2.17.1", "debounce": "2.2.0", "ejs": "3.1.10", "electron-debug": "4.1.0", @@ -84,6 +86,7 @@ "escape-html": "1.0.3", "eslint": "9.17.0", "express": "4.21.2", + "time2fa": "^1.3.0", "express-rate-limit": "7.5.0", "express-session": "1.18.1", "force-graph": "1.47.1", diff --git a/src/app.ts b/src/app.ts index a3943b713..357ad12dc 100644 --- a/src/app.ts +++ b/src/app.ts @@ -14,6 +14,9 @@ import custom from "./routes/custom.js"; import error_handlers from "./routes/error_handlers.js"; import { startScheduledCleanup } from "./services/erase.js"; import sql_init from "./services/sql_init.js"; +import oidc from "express-openid-connect"; +import openID from "./services/open_id.js"; +import * as dotenv from "dotenv"; import { t } from "i18next"; await import('./services/handlers.js'); @@ -23,6 +26,9 @@ const app = express(); const scriptDir = dirname(fileURLToPath(import.meta.url)); +// Configure environment variables +dotenv.config(); + // Initialize DB sql_init.initializeDb(); @@ -57,6 +63,9 @@ app.use(`/icon.png`, express.static(path.join(scriptDir, 'public/icon.png'))); app.use(sessionParser); app.use(favicon(`${scriptDir}/../images/app-icons/icon.ico`)); +if (openID.checkOpenIDRequirements()) + app.use(oidc.auth(openID.generateOAuthConfig())); + await assets.register(app); routes.register(app); custom.register(app); diff --git a/src/errors/open_id_error.ts b/src/errors/open_id_error.ts new file mode 100644 index 000000000..396598b49 --- /dev/null +++ b/src/errors/open_id_error.ts @@ -0,0 +1,9 @@ +class OpenIDError { + message: string; + + constructor(message: string) { + this.message = message; + } +} + +export default OpenIDError; \ No newline at end of file diff --git a/src/public/app/widgets/type_widgets/content_widget.js b/src/public/app/widgets/type_widgets/content_widget.js index e8c5e4829..e509caa32 100644 --- a/src/public/app/widgets/type_widgets/content_widget.js +++ b/src/public/app/widgets/type_widgets/content_widget.js @@ -33,6 +33,7 @@ import BackendLogWidget from "./content/backend_log.js"; import AttachmentErasureTimeoutOptions from "./options/other/attachment_erasure_timeout.js"; import RibbonOptions from "./options/appearance/ribbon.js"; import LocalizationOptions from "./options/appearance/i18n.js"; +import MultiFactorAuthenticationOptions from './options/multi_factor_authentication.js'; import CodeBlockOptions from "./options/appearance/code_block.js"; import EditorOptions from "./options/text_notes/editor.js"; @@ -41,11 +42,11 @@ const TPL = `
.type-contentWidget .note-detail { height: 100%; } - + .note-detail-content-widget { height: 100%; } - + .note-detail-content-widget-content { padding: 15px; height: 100%; @@ -82,6 +83,7 @@ const CONTENT_WIDGETS = { _optionsImages: [ ImageOptions ], _optionsSpellcheck: [ SpellcheckOptions ], _optionsPassword: [ PasswordOptions ], + _optionsMFA: [ MultiFactorAuthenticationOptions ], _optionsEtapi: [ EtapiOptions ], _optionsBackup: [ BackupOptions ], _optionsSync: [ SyncOptions ], diff --git a/src/public/app/widgets/type_widgets/options/multi_factor_authentication.js b/src/public/app/widgets/type_widgets/options/multi_factor_authentication.js new file mode 100644 index 000000000..f4f5ee365 --- /dev/null +++ b/src/public/app/widgets/type_widgets/options/multi_factor_authentication.js @@ -0,0 +1,220 @@ +import server from "../../../services/server.js"; +import toastService from "../../../services/toast.js"; +import OptionsWidget from "./options_widget.js"; + +const TPL = ` +
+

What is Multi-Factor Authentication?

+ + Multi-Factor Authentication (MFA) adds an extra layer of security to your account. Instead + of just entering a password to log in, MFA requires you to provide one or more additional + pieces of evidence to verify your identity. This way, even if someone gets hold of your + password, they still ca TOTP_ENABLED is not set in environment variable. Requires restart.n't access your account without the second piece of information. + It's like adding an extra lock to your door, making it much harder for anyone else to + break in. +
+ +
+

OAuth/OpenID

+ OpenID is a standardized way to let you log into websites using an account from another service, like Google, to verify your identity. +
+ + + +
+
+ User Account: +
+ User Email: +
+
+ +
+

Time-based One-Time Password

+
+ + + +
+
+ TOTP (Time-Based One-Time Password) is a security feature that generates a unique, temporary + code which changes every 30 seconds. You use this code, along with your password to log into your + account, making it much harder for anyone else to access it. +
+
+ +
+

Generate TOTP Secret

+ TOTP Secret Key +
+ +
+ +
+

Single Sign-on Recovery Keys

+ Single sign-on recovery keys are used to login in the event you cannot access your Authenticator codes. Keep them somewhere safe and secure. +

+ After a recovery key is used it cannot be used again. +

+ + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ +
+`; + +export default class MultiFactorAuthenticationOptions extends OptionsWidget { + doRender() { + this.$widget = $(TPL); + + this.$regenerateTotpButton = this.$widget.find(".regenerate-totp"); + this.$totpEnabled = this.$widget.find(".totp-enabled"); + this.$totpSecret = this.$widget.find(".totp-secret"); + this.$totpSecretInput = this.$widget.find(".totp-secret-input"); + this.$authenticatorCode = this.$widget.find(".authenticator-code"); + this.$generateRecoveryCodeButton = this.$widget.find( + ".generate-recovery-code" + ); + this.$oAuthEnabledCheckbox = this.$widget.find(".oauth-enabled-checkbox"); + this.$oauthLoginButton = this.$widget.find(".oauth-login-button"); + this.$UserAccountName = this.$widget.find(".user-account-name"); + this.$UserAccountEmail = this.$widget.find(".user-account-email"); + this.$envEnabledTOTP = this.$widget.find(".env-totp-enabled"); + this.$envEnabledOAuth = this.$widget.find(".env-oauth-enabled"); + + + this.$recoveryKeys = []; + + for (let i = 0; i < 8; i++) + { + this.$recoveryKeys.push(this.$widget.find(".key_" + i)); + } + + this.$generateRecoveryCodeButton.on("click", async () => { + this.setRecoveryKeys(); + }); + + this.$regenerateTotpButton.on("click", async () => { + this.generateKey(); + }); + + this.$protectedSessionTimeout = this.$widget.find( + ".protected-session-timeout-in-seconds" + ); + this.$protectedSessionTimeout.on("change", () => + this.updateOption( + "protectedSessionTimeout", + this.$protectedSessionTimeout.val() + ) + ); + + this.displayRecoveryKeys(); + } + + async setRecoveryKeys() { + server.get("totp_recovery/generate").then((result) => { + if (!result.success) { + toastService.showError("Error in revevery code generation!"); + return; + } + this.keyFiller(result.recoveryCodes); + server.post("totp_recovery/set", { + recoveryCodes: result.recoveryCodes, + }); + }); + } + + async keyFiller(values) { + // Forces values to be a string so it doesn't error out when I split. + // Will be a non-issue when I update everything to typescript. + const keys = (values + "").split(","); + for (let i = 0; i < keys.length; i++) this.$recoveryKeys[i].text(keys[i]); + } + + async generateKey() { + server.get("totp/generate").then((result) => { + if (result.success) { + this.$totpSecret.text(result.message); + } else { + toastService.showError(result.message); + } + }); + } + + optionsLoaded(options) { + server.get("oauth/status").then((result) => { + if (result.enabled) { + this.$oAuthEnabledCheckbox.prop("checked", result.enabled); + this.$UserAccountName.text(result.name); + this.$UserAccountEmail.text(result.email); + }else + this.$envEnabledOAuth.text( + "set SSO_ENABLED as environment variable to 'true' to enable (Requires restart)" + ); + }); + + server.get("totp/status").then((result) => { + if (result.enabled){ + this.$totpEnabled.prop("checked", result.message); + this.$authenticatorCode.prop("disabled", !result.message); + this.$generateRecoveryCodeButton.prop("disabled", !result.message); + } + else { + this.$totpEnabled.prop("checked", false); + this.$totpEnabled.prop("disabled", true); + this.$authenticatorCode.prop("disabled", true); + this.$generateRecoveryCodeButton.prop("disabled", true); + + this.$envEnabledTOTP.text( + "Set TOTP_ENABLED as environment variable to 'true' to enable (Requires restart)" + ); + } + }); + this.$protectedSessionTimeout.val(options.protectedSessionTimeout); + } + + displayRecoveryKeys() { + server.get("totp_recovery/enabled").then((result) => { + if (!result.success) { + this.keyFiller(Array(8).fill("Error generating recovery keys!")); + return; + } + + if (!result.keysExist) { + this.keyFiller(Array(8).fill("No key set")); + this.$generateRecoveryCodeButton.text("Generate Recovery Codes"); + return; + } + }); + server.get("totp_recovery/used").then((result) => { + this.keyFiller((result.usedRecoveryCodes + "").split(",")); + this.$generateRecoveryCodeButton.text("Regenerate Recovery Codes"); + }); + } +} diff --git a/src/routes/api/recovery_codes.ts b/src/routes/api/recovery_codes.ts new file mode 100644 index 000000000..057914388 --- /dev/null +++ b/src/routes/api/recovery_codes.ts @@ -0,0 +1,51 @@ +import recovery_codes from'../../services/encryption/recovery_codes.js'; +import {Request} from 'express'; +import {randomBytes} from 'crypto'; + +function setRecoveryCodes(req: Request) { + const success = recovery_codes.setRecoveryCodes(req.body.recoveryCodes); + return {success: success, message: 'Recovery codes set!'}; +} + +function veryifyRecoveryCode(req: Request) { + const success = recovery_codes.verifyRecoveryCode(req.body.recovery_code_guess); + + return {success: success}; +} + +function checkForRecoveryKeys() { + return {success +: true, keysExist: recovery_codes.isRecoveryCodeSet()}; +} + +function generateRecoveryCodes() { + const recoveryKeys = [ + randomBytes(16).toString('base64'), + randomBytes(16).toString('base64'), + randomBytes(16).toString('base64'), + randomBytes(16).toString('base64'), + randomBytes(16).toString('base64'), + randomBytes(16).toString('base64'), + randomBytes(16).toString('base64'), + randomBytes(16).toString('base64') + ]; + + recovery_codes.setRecoveryCodes(recoveryKeys.toString()); + + return {success: true, recoveryCodes: recoveryKeys.toString()}; +} + +function getUsedRecoveryCodes() { + return { + success: true, + usedRecoveryCodes: recovery_codes.getUsedRecoveryCodes().toString() + }; +} + +export default { + setRecoveryCodes, + generateRecoveryCodes, + veryifyRecoveryCode, + checkForRecoveryKeys, + getUsedRecoveryCodes +}; \ No newline at end of file diff --git a/src/routes/api/totp.ts b/src/routes/api/totp.ts new file mode 100644 index 000000000..6435b796b --- /dev/null +++ b/src/routes/api/totp.ts @@ -0,0 +1,31 @@ +import {generateSecret} from 'time2fa'; + +function generateTOTPSecret() { + return {success: 'true', message: generateSecret()}; +} + +function getTotpEnabled() { + if (process.env.TOTP_ENABLED === undefined) { + return false; + } + if (process.env.TOTP_ENABLED.toLocaleLowerCase() !== 'true') { + return false; + } + + return true; +} + +function getTOTPStatus() { + const totpEnabled = getTotpEnabled(); + return {success: true, message: totpEnabled, enabled: getTotpEnabled()}; +} + +function getSecret() { + return process.env.TOTP_SECRET; +} + +export default { + generateSecret: generateTOTPSecret, + getTOTPStatus, + getSecret +}; \ No newline at end of file diff --git a/src/routes/login.ts b/src/routes/login.ts index 4af0abda3..94c13e028 100644 --- a/src/routes/login.ts +++ b/src/routes/login.ts @@ -9,16 +9,26 @@ import assetPath from "../services/asset_path.js"; import appPath from "../services/app_path.js"; import ValidationError from "../errors/validation_error.js"; import { Request, Response } from 'express'; +import recoveryCodeService from '../services/encryption/recovery_codes.js'; +import openIDService from '../services/open_id.js'; +import openIDEncryption from '../services/encryption/open_id_encryption.js'; +import totp from '../services/totp.js'; +import open_id from '../services/open_id.js'; function loginPage(req: Request, res: Response) { - res.render('login', { + if (open_id.isOpenIDEnabled()) { + res.redirect('/authenticate'); + } else { + res.render('login', { failedAuth: false, + totpEnabled: totp.isTotpEnabled(), assetPath: assetPath, - appPath: appPath - }); -} + appPath: appPath, + }); + } + } -function setPasswordPage(req: Request, res: Response) { + function setPasswordPage(req: Request, res: Response) { res.render('set_password', { error: false, assetPath: assetPath, @@ -58,8 +68,16 @@ function setPassword(req: Request, res: Response) { function login(req: Request, res: Response) { const guessedPassword = req.body.password; + const guessedTotp = req.body.token; if (verifyPassword(guessedPassword)) { + if (totp.isTotpEnabled()){ + if (!verifyTOTP(guessedTotp)) { + sendLoginError(req, res); + return; + } + } + const rememberMe = req.body.rememberMe; req.session.regenerate(() => { @@ -74,16 +92,18 @@ function login(req: Request, res: Response) { }); } else { - // note that logged IP address is usually meaningless since the traffic should come from a reverse proxy - log.info(`WARNING: Wrong password from ${req.ip}, rejecting.`); - - res.status(401).render('login', { - failedAuth: true, - assetPath: assetPath - }); + sendLoginError(req, res); } } +function verifyTOTP(guessedToken: string) { + if (totp.validateTOTP(guessedToken)) return true; + + const recoveryCodeValidates = recoveryCodeService.verifyRecoveryCode(guessedToken); + + return recoveryCodeValidates; +} + function verifyPassword(guessedPassword: string) { const hashed_password = utils.fromBase64(optionService.getOption('passwordVerificationHash')); @@ -92,11 +112,27 @@ function verifyPassword(guessedPassword: string) { return guess_hashed.equals(hashed_password); } +function sendLoginError(req: Request, res: Response) { + // note that logged IP address is usually meaningless since the traffic should come from a reverse proxy + if ( totp.isTotpEnabled( )){ + log.info(`WARNING: Wrong password or TOTP from ${req.ip}, rejecting.`); + }else{ + log.info(`WARNING: Wrong password from ${req.ip}, rejecting.`); + } + + res.status(401).render('login', { + failedAuth: true, + totpEnabled: optionService.getOption('totpEnabled') && totp.checkForTotSecret(), + assetPath: assetPath, + }); +} + function logout(req: Request, res: Response) { req.session.regenerate(() => { req.session.loggedIn = false; - - res.redirect('login'); + if (openIDService.isOpenIDEnabled() && openIDEncryption.isSubjectIdentifierSaved()) { + res.oidc.logout({ returnTo: '/authenticate' }); + } else res.redirect('login'); }); } diff --git a/src/routes/routes.ts b/src/routes/routes.ts index ac26999f6..7c07708c5 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -6,6 +6,9 @@ import log from "../services/log.js"; import express from "express"; const router = express.Router(); import auth from "../services/auth.js"; +import openID from '../services/open_id.js'; +import totp from './api/totp.js'; +import recoveryCodes from './api/recovery_codes.js'; import cls from "../services/cls.js"; import sql from "../services/sql.js"; import entityChangesService from "../services/entity_changes.js"; @@ -71,6 +74,7 @@ import etapiSpecialNoteRoutes from "../etapi/special_notes.js"; import etapiSpecRoute from "../etapi/spec.js"; import etapiBackupRoute from "../etapi/backup.js"; + const csrfMiddleware = csurf({ cookie: { path: "" // empty, so cookie is valid only for the current path @@ -117,6 +121,20 @@ function register(app: express.Application) { route(PST, '/set-password', [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPassword); route(GET, '/setup', [], setupRoute.setupPage); + + apiRoute(GET, '/api/totp/generate', totp.generateSecret); + apiRoute(GET, '/api/totp/status', totp.getTOTPStatus); + apiRoute(GET, '/api/totp/get', totp.getSecret); + + apiRoute(GET, '/api/oauth/status', openID.getOAuthStatus); + apiRoute(GET, '/api/oauth/validate', openID.isTokenValid); + + apiRoute(PST, '/api/totp_recovery/set', recoveryCodes.setRecoveryCodes); + apiRoute(PST, '/api/totp_recovery/verify', recoveryCodes.veryifyRecoveryCode); + apiRoute(GET, '/api/totp_recovery/generate', recoveryCodes.generateRecoveryCodes); + apiRoute(GET, '/api/totp_recovery/enabled', recoveryCodes.checkForRecoveryKeys); + apiRoute(GET, '/api/totp_recovery/used', recoveryCodes.getUsedRecoveryCodes); + apiRoute(GET, '/api/tree', treeApiRoute.getTree); apiRoute(PST, '/api/tree/load', treeApiRoute.load); diff --git a/src/services/auth.ts b/src/services/auth.ts index b7f183fc4..ba416902b 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -8,17 +8,32 @@ import passwordEncryptionService from "./encryption/password_encryption.js"; import config from "./config.js"; import passwordService from "./encryption/password.js"; import type { NextFunction, Request, Response } from 'express'; +import openID from './open_id.js'; +import open_id_encryption from './encryption/open_id_encryption.js'; const noAuthentication = config.General && config.General.noAuthentication === true; function checkAuth(req: Request, res: Response, next: NextFunction) { if (!sqlInit.isDbInitialized()) { - res.redirect("setup"); - } - else if (!req.session.loggedIn && !utils.isElectron() && !noAuthentication) { - res.redirect("login"); - } - else { + res.redirect('setup'); + } else if (openID.checkOpenIDRequirements()) { + if ( + req.oidc.isAuthenticated() && + open_id_encryption.verifyOpenIDSubjectIdentifier(req.oidc.user?.sub) + ) { + req.session.loggedIn = true; + next(); + } else { + req.session.loggedIn = false; + res.oidc.login({}); + } + } else if ( + !req.session.loggedIn && + !utils.isElectron() && + !noAuthentication + ) { + res.redirect('login'); + } else { next(); } } diff --git a/src/services/encryption/my_scrypt.ts b/src/services/encryption/my_scrypt.ts index 658f4f230..589da6eed 100644 --- a/src/services/encryption/my_scrypt.ts +++ b/src/services/encryption/my_scrypt.ts @@ -2,6 +2,7 @@ import optionService from "../options.js"; import crypto from "crypto"; +import sql from "../sql.js"; function getVerificationHash(password: crypto.BinaryLike) { const salt = optionService.getOption('passwordVerificationSalt'); @@ -22,7 +23,45 @@ function getScryptHash(password: crypto.BinaryLike, salt: crypto.BinaryLike) { return hashed; } +function getSubjectIdentifierVerificationHash( + guessedUserId: string | crypto.BinaryLike, + salt?: string +) { + if (salt != null) return getScryptHash(guessedUserId, salt); + + const savedSalt = sql.getValue("SELECT salt FROM user_data;"); + if (savedSalt === undefined || savedSalt === null) { + console.log("User salt undefined!"); + return undefined; + } + return getScryptHash(guessedUserId, savedSalt.toString()); +} + +function getSubjectIdentifierDerivedKey( + subjectIdentifer: crypto.BinaryLike, + givenSalt?: string +) { + if (givenSalt !== undefined) { + return getScryptHash(subjectIdentifer, givenSalt.toString()); + } + + const salt = sql.getValue("SELECT salt FROM user_data;"); + if (salt === undefined || salt === null) return undefined; + + return getScryptHash(subjectIdentifer, salt.toString()); +} + +function createSubjectIdentifierDerivedKey( + subjectIdentifer: string | crypto.BinaryLike, + salt: string | crypto.BinaryLike +) { + return getScryptHash(subjectIdentifer, salt); +} + export default { getVerificationHash, - getPasswordDerivedKey + getPasswordDerivedKey, + getSubjectIdentifierVerificationHash, + getSubjectIdentifierDerivedKey, + createSubjectIdentifierDerivedKey }; diff --git a/src/services/encryption/open_id_encryption.ts b/src/services/encryption/open_id_encryption.ts new file mode 100644 index 000000000..76b1812b7 --- /dev/null +++ b/src/services/encryption/open_id_encryption.ts @@ -0,0 +1,145 @@ +import myScryptService from "./my_scrypt.js"; +import utils from "../utils.js"; +import dataEncryptionService from "./data_encryption.js"; +import sql from "../sql.js"; +import sqlInit from "../sql_init.js"; +import OpenIDError from "../../errors/open_id_error.js"; + +function saveUser(subjectIdentifier: string, name: string, email: string) { + if (isUserSaved()) return false; + + const verificationSalt = utils.randomSecureToken(32); + const derivedKeySalt = utils.randomSecureToken(32); + + const verificationHash = myScryptService.getSubjectIdentifierVerificationHash( + subjectIdentifier, + verificationSalt + ); + if (verificationHash === undefined) { + throw new OpenIDError("Verification hash undefined!") + } + + const userIDEncryptedDataKey = setDataKey( + subjectIdentifier, + utils.randomSecureToken(16), + verificationSalt + ); + + if (userIDEncryptedDataKey === undefined || userIDEncryptedDataKey === null) { + console.log("USERID ENCRYPTED DATA KEY NULL"); + return undefined; + } + + const data = { + tmpID: 0, + userIDVerificationHash: utils.toBase64(verificationHash), + salt: verificationSalt, + derivedKey: derivedKeySalt, + userIDEcnryptedDataKey: userIDEncryptedDataKey, + isSetup: "true", + username: name, + email: email + }; + + sql.upsert("user_data", "tmpID", data); + return true; +} + +function isSubjectIdentifierSaved() { + const value = sql.getValue("SELECT userIDEcnryptedDataKey FROM user_data;"); + if (value === undefined || value === null || value === "") return false; + return true; +} + +function isUserSaved() { + const isSaved = sql.getValue("SELECT isSetup FROM user_data;"); + return isSaved === "true" ? true : false; +} + +function verifyOpenIDSubjectIdentifier(subjectIdentifier: string) { + if (!sqlInit.isDbInitialized()) { + throw new OpenIDError("Database not initialized!"); + } + + if (isUserSaved()) { + return false; + } + + const salt = sql.getValue("SELECT salt FROM user_data;"); + if (salt == undefined) { + console.log("Salt undefined"); + return undefined; + } + + const givenHash = myScryptService + .getSubjectIdentifierVerificationHash(subjectIdentifier) + ?.toString("base64"); + if (givenHash === undefined) { + console.log("Sub id hash undefined!"); + return undefined; + } + + const savedHash = sql.getValue( + "SELECT userIDVerificationHash FROM user_data" + ); + if (savedHash === undefined) { + console.log("verification hash undefined"); + return undefined; + } + + console.log("Matches: " + givenHash === savedHash); + return givenHash === savedHash; +} + +function setDataKey( + subjectIdentifier: string, + plainTextDataKey: string | Buffer, + salt: string +) { + const subjectIdentifierDerivedKey = + myScryptService.getSubjectIdentifierDerivedKey(subjectIdentifier, salt); + + if (subjectIdentifierDerivedKey === undefined) { + console.log("SOMETHING WENT WRONG SAVING USER ID DERIVED KEY"); + return undefined; + } + const newEncryptedDataKey = dataEncryptionService.encrypt( + subjectIdentifierDerivedKey, + plainTextDataKey + ); + + return newEncryptedDataKey; +} + +function getDataKey(subjectIdentifier: string) { + const subjectIdentifierDerivedKey = + myScryptService.getSubjectIdentifierDerivedKey(subjectIdentifier); + + const encryptedDataKey = sql.getValue( + "SELECT userIDEcnryptedDataKey FROM user_data" + ); + + if (encryptedDataKey === undefined || encryptedDataKey === null) { + console.log("Encrypted data key empty!"); + return undefined; + } + + if (subjectIdentifierDerivedKey === undefined) { + console.log("SOMETHING WENT WRONG SAVING USER ID DERIVED KEY"); + return undefined; + } + const decryptedDataKey = dataEncryptionService.decrypt( + subjectIdentifierDerivedKey, + encryptedDataKey.toString() + ); + + return decryptedDataKey; +} + +export default { + verifyOpenIDSubjectIdentifier, + getDataKey, + setDataKey, + saveUser, + isSubjectIdentifierSaved, +}; \ No newline at end of file diff --git a/src/services/encryption/recovery_codes.ts b/src/services/encryption/recovery_codes.ts new file mode 100644 index 000000000..c03256a83 --- /dev/null +++ b/src/services/encryption/recovery_codes.ts @@ -0,0 +1,90 @@ +'use strict'; + +import sql from '../sql.js'; +import optionService from '../options.js'; +import crypto from 'crypto'; + +function isRecoveryCodeSet() { + return optionService.getOptionBool('encryptedRecoveryCodes'); +} + +function setRecoveryCodes(recoveryCodes: string) { + const iv = crypto.randomBytes(16); + const securityKey = crypto.randomBytes(32); + const cipher = crypto.createCipheriv('aes-256-cbc', securityKey, iv); + let encryptedRecoveryCodes = cipher.update(recoveryCodes, 'utf-8', 'hex'); + + sql.transactional(() => { + optionService.setOption('recoveryCodeInitialVector', iv.toString('hex')); + optionService.setOption('recoveryCodeSecurityKey', securityKey.toString('hex')); + optionService.setOption('recoveryCodesEncrypted', encryptedRecoveryCodes + cipher.final('hex')); + optionService.setOption('encryptedRecoveryCodes', 'true'); + return true; + }); + return false; +} +function getRecoveryCodes() { + if (!isRecoveryCodeSet()) { + return Array(8).fill("Keys not set") + } + + return sql.transactional(() => { + const iv = Buffer.from(optionService.getOption('recoveryCodeInitialVector'), 'hex'); + const securityKey = Buffer.from(optionService.getOption('recoveryCodeSecurityKey'), 'hex'); + const encryptedRecoveryCodes = optionService.getOption('recoveryCodesEncrypted'); + + const decipher = crypto.createDecipheriv('aes-256-cbc', securityKey, iv); + const decryptedData = decipher.update(encryptedRecoveryCodes, 'hex', 'utf-8'); + + const decryptedString = decryptedData + decipher.final('utf-8'); + return decryptedString.split(','); + }); +} + +function removeRecoveryCode(usedCode: string) { + const oldCodes: string[] = getRecoveryCodes(); + const today = new Date(); + oldCodes[oldCodes.indexOf(usedCode)] = today.toJSON().replace(/-/g, '/'); + setRecoveryCodes(oldCodes.toString()); +} + +function verifyRecoveryCode(recoveryCodeGuess: string) { + const recoveryCodeRegex = RegExp(/^.{22}==$/gm); + if (!recoveryCodeRegex.test(recoveryCodeGuess)) { + return false; + } + + const recoveryCodes = getRecoveryCodes(); + var loginSuccess = false; + recoveryCodes.forEach((recoveryCode: string) => { + if (recoveryCodeGuess === recoveryCode) { + removeRecoveryCode(recoveryCode); + loginSuccess = true; + return; + } + }); + return loginSuccess; +} + +function getUsedRecoveryCodes() { + if (!isRecoveryCodeSet()){ + return Array(8).fill("Recovery code not set") + } + + const dateRegex = RegExp(/^\d{4}\/\d{2}\/\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/gm); + const recoveryCodes = getRecoveryCodes(); + const usedStatus: string[] = []; + + recoveryCodes.forEach((recoveryKey: string) => { + if (dateRegex.test(recoveryKey)) usedStatus.push('Used: ' + recoveryKey); + else usedStatus.push('Recovery code ' + recoveryCodes.indexOf(recoveryKey) + ' is unused'); + }); + return usedStatus; +} + +export default { + setRecoveryCodes, + verifyRecoveryCode, + getUsedRecoveryCodes, + isRecoveryCodeSet +}; \ No newline at end of file diff --git a/src/services/hidden_subtree.ts b/src/services/hidden_subtree.ts index 9e29375fd..69db1e615 100644 --- a/src/services/hidden_subtree.ts +++ b/src/services/hidden_subtree.ts @@ -266,6 +266,7 @@ function buildHiddenSubtreeDefinition(): Item { { id: '_optionsImages', title: t("hidden-subtree.images-title"), type: 'contentWidget', icon: 'bx-image' }, { id: '_optionsSpellcheck', title: t("hidden-subtree.spellcheck-title"), type: 'contentWidget', icon: 'bx-check-double' }, { id: '_optionsPassword', title: t("hidden-subtree.password-title"), type: 'contentWidget', icon: 'bx-lock' }, + { id: '_optionsMFA', title: 'MFA', type: 'contentWidget', icon: 'bx-lock '}, { id: '_optionsEtapi', title: t("hidden-subtree.etapi-title"), type: 'contentWidget', icon: 'bx-extension' }, { id: '_optionsBackup', title: t("hidden-subtree.backup-title"), type: 'contentWidget', icon: 'bx-data' }, { id: '_optionsSync', title: t("hidden-subtree.sync-title"), type: 'contentWidget', icon: 'bx-wifi' }, diff --git a/src/services/open_id.ts b/src/services/open_id.ts new file mode 100644 index 000000000..27e879452 --- /dev/null +++ b/src/services/open_id.ts @@ -0,0 +1,154 @@ +import OpenIDError from "../errors/open_id_error.js"; +import { NextFunction, Request, Response } from "express"; +import openIDEncryption from "./encryption/open_id_encryption.js"; +import sqlInit from "./sql_init.js"; +import options from "./options.js"; +import { Session, auth } from "express-openid-connect"; +import sql from "./sql.js"; + +function isOpenIDEnabled() { + return checkOpenIDRequirements(); +} + +function isUserSaved() { + const data = sql.getValue("SELECT isSetup FROM user_data;"); + return data === "true" ? true : false; +} + +function getUsername() { + const username = sql.getValue("SELECT username FROM user_data;"); + return username; +} + +function getUserEmail() { + const email = sql.getValue("SELECT email FROM user_data;"); + return email; +} + +function clearSavedUser() { + sql.execute("DELETE FROM user_data"); + options.setOption("isUserSaved", false); + return { + success: true, + message: "Account data removed." + }; +} + +function checkOpenIDRequirements() { + if (process.env.SSO_ENABLED === undefined) { + return false; + } + if (process.env.SSO_ENABLED.toLocaleLowerCase() !== "true") { + return false; + } + + if (process.env.TOTP_ENABLED?.toLocaleLowerCase() === "true"){ + throw new OpenIDError("Cannot enable both OpenID and TOTP!"); + } + + if (process.env.BASE_URL === undefined) { + throw new OpenIDError("BASE_URL is undefined in .env!"); + } + if (process.env.CLIENT_ID === undefined) { + throw new OpenIDError("CLIENT_ID is undefined in .env!"); + } + if (process.env.SECRET === undefined) { + throw new OpenIDError("SECRET is undefined in .env!"); + } + + return true; +} + +function getOAuthStatus() { + return { + success: true, + name: getUsername(), + email: getUserEmail(), + enabled: isOpenIDEnabled(), + }; +} + +function isTokenValid(req: Request, res: Response, next: NextFunction) { + const userStatus = openIDEncryption.isSubjectIdentifierSaved(); + + if (req.oidc !== undefined) { + const result = req.oidc + .fetchUserInfo() + .then((result) => { + return { + success: true, + message: "Token is valid", + user: userStatus, + }; + }) + .catch((result) => { + return { + success: false, + message: "Token is not valid", + user: userStatus, + }; + }); + return result; + } else { + return { + success: false, + message: "Token not set up", + user: userStatus, + }; + } +} + +function generateOAuthConfig() { + const authRoutes = { + callback: "/callback", + login: "/authenticate", + postLogoutRedirect: "/login", + logout: "/logout", + }; + + const logoutParams = { + }; + + const authConfig = { + authRequired: true, + auth0Logout: false, + baseURL: process.env.BASE_URL, + clientID: process.env.CLIENT_ID, + issuerBaseURL: "https://accounts.google.com/.well-known/openid-configuration", + secret: process.env.SECRET, + clientSecret: process.env.SECRET, + authorizationParams: { + response_type: "code", + scope: "openid profile email", + }, + routes: authRoutes, + idpLogout: false, + logoutParams: logoutParams, + afterCallback: async (req: Request, res: Response, session: Session) => { + if (!sqlInit.isDbInitialized()) return session; + + if (isUserSaved()) return session; + + if (req.oidc.user === undefined) { + console.log("user invalid!"); + }else { + openIDEncryption.saveUser( + req.oidc.user.sub.toString(), + req.oidc.user.name.toString(), + req.oidc.user.email.toString()); + } + return session; + }, + }; + return authConfig; +} + +export default { + generateOAuthConfig, + getOAuthStatus, + isOpenIDEnabled, + clearSavedUser, + checkOpenIDRequirements, + isTokenValid, + isUserSaved, +}; \ No newline at end of file diff --git a/src/services/options_init.ts b/src/services/options_init.ts index a48de12e3..43dd700a6 100644 --- a/src/services/options_init.ts +++ b/src/services/options_init.ts @@ -117,6 +117,10 @@ const defaultOptions: DefaultOption[] = [ { name: 'customSearchEngineUrl', value: 'https://duckduckgo.com/?q={keyword}', isSynced: true }, { name: 'promotedAttributesOpenInRibbon', value: 'true', isSynced: true }, { name: 'editedNotesOpenInRibbon', value: 'true', isSynced: true }, + { name: 'totpEnabled', value: 'false', isSynced: true}, + { name: 'encryptedRecoveryCodes', value: 'false', isSynced: true}, + { name: 'userSubjectIdentifierSaved', value: 'false', isSynced: true}, + { name: 'oAuthEnabled', value: 'false', isSynced: true}, // Internationalization { name: 'locale', value: 'en', isSynced: true }, diff --git a/src/services/sql_init.ts b/src/services/sql_init.ts index 0bcf77002..095e38220 100644 --- a/src/services/sql_init.ts +++ b/src/services/sql_init.ts @@ -47,6 +47,21 @@ async function initDbConnection() { sql.execute('CREATE TEMP TABLE "param_list" (`paramId` TEXT NOT NULL PRIMARY KEY)'); + sql.execute(` + CREATE TABLE IF NOT EXISTS "user_data" + ( + tmpID INT, + username TEXT, + email TEXT, + userIDEcnryptedDataKey TEXT, + userIDVerificationHash TEXT, + salt TEXT, + derivedKey TEXT, + isSetup TEXT DEFAULT "false", + UNIQUE (tmpID), + PRIMARY KEY (tmpID) + );`) + dbReady.resolve(); } diff --git a/src/services/totp.ts b/src/services/totp.ts new file mode 100644 index 000000000..71580fb92 --- /dev/null +++ b/src/services/totp.ts @@ -0,0 +1,47 @@ +'use strict'; + +import {Totp} from 'time2fa'; + +function isTotpEnabled() { + if (process.env.TOTP_ENABLED === undefined) { + return false; + } + if (process.env.TOTP_SECRET === undefined) { + return false; + } + if (process.env.TOTP_ENABLED.toLocaleLowerCase() !== 'true') { + return false; + } + + return true; +} + +function getTotpSecret() { + return process.env.TOTP_SECRET; +} + +function checkForTotSecret() { + if (process.env.TOTP_SECRET !== undefined) return true; + else return false; +} + +function validateTOTP(guessedPasscode: string) { + if (process.env.TOTP_SECRET === undefined) return false; + + try { + const valid = Totp.validate({ + passcode: guessedPasscode, + secret: process.env.TOTP_SECRET.trim() + }); + return valid; + } catch (e) { + return false; + } +} + +export default { + isTotpEnabled, + getTotpSecret, + checkForTotSecret, + validateTOTP +}; \ No newline at end of file diff --git a/src/views/login.ejs b/src/views/login.ejs index a17a58909..54b38e0b8 100644 --- a/src/views/login.ejs +++ b/src/views/login.ejs @@ -13,10 +13,17 @@

<%= t("login.heading") %>

<% if (failedAuth) { %> -
- <%= t("login.incorrect-password") %> -
+ <% if( totpEnabled ) { %> +
+ Password or TOTP is incorrect. Please try again. +
+ <% }else{ %> +
+ Password is incorrect. Please try again. +
+ <% } %> <% } %> +
@@ -25,6 +32,15 @@
+ <% if( totpEnabled ) { %> +
+ +
+ +
+
+ <% } %>