mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-27 18:12:29 +08:00
Ported from branch OIDC
This commit is contained in:
parent
1c8cc36402
commit
9c748f326a
2
.gitignore
vendored
2
.gitignore
vendored
@ -32,3 +32,5 @@ images/app-icons/mac/*.png
|
|||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
/playwright/.auth/
|
/playwright/.auth/
|
||||||
|
|
||||||
|
.env
|
@ -126,6 +126,17 @@ CREATE TABLE IF NOT EXISTS "attachments"
|
|||||||
utcDateScheduledForErasureSince TEXT DEFAULT NULL,
|
utcDateScheduledForErasureSince TEXT DEFAULT NULL,
|
||||||
isDeleted INT not null,
|
isDeleted INT not null,
|
||||||
deleteId TEXT DEFAULT NULL);
|
deleteId TEXT DEFAULT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_data"
|
||||||
|
(
|
||||||
|
tmpID INT,
|
||||||
|
userIDEcnryptedDataKey TEXT,
|
||||||
|
userIDVerificationHash TEXT,
|
||||||
|
salt TEXT,
|
||||||
|
derivedKey TEXT,
|
||||||
|
isSetup TEXT DEFAULT "false",
|
||||||
|
UNIQUE (tmpID),
|
||||||
|
PRIMARY KEY (tmpID)
|
||||||
|
);
|
||||||
CREATE INDEX IDX_attachments_ownerId_role
|
CREATE INDEX IDX_attachments_ownerId_role
|
||||||
on attachments (ownerId, role);
|
on attachments (ownerId, role);
|
||||||
|
|
||||||
|
9
example.env
Normal file
9
example.env
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
OAUTH_ENABLED="false"
|
||||||
|
BASE_URL="http://localhost:8080"
|
||||||
|
CLIENT_ID="1234"
|
||||||
|
ISSUER_BASE_URL="https://example.com/xyz/.well-known/openid-configuration"
|
||||||
|
SECRET="I-Like-Trilium-Notes"
|
||||||
|
AUTH_0_LOGOUT="false"
|
||||||
|
|
||||||
|
TOTP_ENABLED="false"
|
||||||
|
TOTP_SECRET="Trilium-Notes-is-the-best"
|
221
package-lock.json
generated
221
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "trilium",
|
"name": "trilium",
|
||||||
"version": "0.90.4",
|
"version": "0.90.5-beta",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "trilium",
|
"name": "trilium",
|
||||||
"version": "0.90.4",
|
"version": "0.90.5-beta",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "^7.1.0",
|
"@braintree/sanitize-url": "^7.1.0",
|
||||||
@ -37,6 +37,7 @@
|
|||||||
"escape-html": "1.0.3",
|
"escape-html": "1.0.3",
|
||||||
"eslint": "^9.9.0",
|
"eslint": "^9.9.0",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
"express-openid-connect": "^2.17.1",
|
||||||
"express-partial-content": "1.0.2",
|
"express-partial-content": "1.0.2",
|
||||||
"express-rate-limit": "^7.3.1",
|
"express-rate-limit": "^7.3.1",
|
||||||
"express-session": "1.18.0",
|
"express-session": "1.18.0",
|
||||||
@ -88,6 +89,7 @@
|
|||||||
"split.js": "1.6.5",
|
"split.js": "1.6.5",
|
||||||
"stream-throttle": "0.1.3",
|
"stream-throttle": "0.1.3",
|
||||||
"striptags": "3.2.0",
|
"striptags": "3.2.0",
|
||||||
|
"time2fa": "^1.3.0",
|
||||||
"tmp": "0.2.3",
|
"tmp": "0.2.3",
|
||||||
"tree-kill": "1.2.2",
|
"tree-kill": "1.2.2",
|
||||||
"turndown": "^7.2.0",
|
"turndown": "^7.2.0",
|
||||||
@ -2387,6 +2389,19 @@
|
|||||||
"integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
|
"integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"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=="
|
||||||
|
},
|
||||||
|
"node_modules/@hapi/topo": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@hapi/hoek": "^9.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/module-importer": {
|
"node_modules/@humanwhocodes/module-importer": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||||
@ -3038,6 +3053,14 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"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==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pkgjs/parseargs": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@ -3062,6 +3085,24 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"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=="
|
||||||
|
},
|
||||||
|
"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=="
|
||||||
|
},
|
||||||
"node_modules/@sindresorhus/is": {
|
"node_modules/@sindresorhus/is": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||||
@ -4053,7 +4094,6 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
||||||
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
|
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clean-stack": "^2.0.0",
|
"clean-stack": "^2.0.0",
|
||||||
"indent-string": "^4.0.0"
|
"indent-string": "^4.0.0"
|
||||||
@ -4707,6 +4747,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/base64url": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bcrypt-pbkdf": {
|
"node_modules/bcrypt-pbkdf": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||||
@ -5413,7 +5461,6 @@
|
|||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||||
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@ -8192,6 +8239,74 @@
|
|||||||
"node": ">= 0.10.0"
|
"node": ">= 0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"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==",
|
||||||
|
"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==",
|
||||||
|
"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==",
|
||||||
|
"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/setprototypeof": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express-partial-content": {
|
"node_modules/express-partial-content": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/express-partial-content/-/express-partial-content-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/express-partial-content/-/express-partial-content-1.0.2.tgz",
|
||||||
@ -8947,6 +9062,14 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/galactus": {
|
"node_modules/galactus": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/galactus/-/galactus-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/galactus/-/galactus-1.0.0.tgz",
|
||||||
@ -10366,7 +10489,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -10865,11 +10987,37 @@
|
|||||||
"regenerator-runtime": "^0.13.3"
|
"regenerator-runtime": "^0.13.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/joi": {
|
||||||
|
"version": "17.13.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
|
||||||
|
"integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
|
||||||
|
"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": {
|
"node_modules/joplin-turndown-plugin-gfm": {
|
||||||
"version": "1.0.12",
|
"version": "1.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/joplin-turndown-plugin-gfm/-/joplin-turndown-plugin-gfm-1.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/joplin-turndown-plugin-gfm/-/joplin-turndown-plugin-gfm-1.0.12.tgz",
|
||||||
"integrity": "sha512-qL4+1iycQjZ1fs8zk3jSRk7cg3ROBUHk7GKtiLAQLFzLPKErnILUvz5DLszSQvz3s1sTjPbywLDISVUtBY6HaA=="
|
"integrity": "sha512-qL4+1iycQjZ1fs8zk3jSRk7cg3ROBUHk7GKtiLAQLFzLPKErnILUvz5DLszSQvz3s1sTjPbywLDISVUtBY6HaA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/jose": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-5hFWIigKqC+e/lRyQhfnirrAqUdIPMB7SJRqflJaO29dW7q5DFvH1XCSTmv6PQ6pb++0k6MJlLRoS0Wv4s38Wg==",
|
||||||
|
"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": {
|
"node_modules/jpeg-js": {
|
||||||
"version": "0.4.4",
|
"version": "0.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||||
@ -11536,6 +11684,17 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"dependencies": {
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lzma-native": {
|
"node_modules/lzma-native": {
|
||||||
"version": "8.0.5",
|
"version": "8.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/lzma-native/-/lzma-native-8.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/lzma-native/-/lzma-native-8.0.5.tgz",
|
||||||
@ -11592,8 +11751,7 @@
|
|||||||
"node_modules/make-error": {
|
"node_modules/make-error": {
|
||||||
"version": "1.3.6",
|
"version": "1.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/make-fetch-happen": {
|
"node_modules/make-fetch-happen": {
|
||||||
"version": "10.2.0",
|
"version": "10.2.0",
|
||||||
@ -12963,6 +13121,14 @@
|
|||||||
"node": ">=0.10.0"
|
"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==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.1",
|
"version": "1.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
||||||
@ -12980,6 +13146,14 @@
|
|||||||
"node": ">= 0.4"
|
"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==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^10.13.0 || >=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/omggif": {
|
"node_modules/omggif": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
|
||||||
@ -13035,6 +13209,26 @@
|
|||||||
"opencollective-postinstall": "index.js"
|
"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==",
|
||||||
|
"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/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@ -15708,6 +15902,11 @@
|
|||||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/time2fa": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/time2fa/-/time2fa-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-gwwawXyW9UalZy+HhsQFLL70MLjvzohXqpbE4EYLVeRxrnUkSvKCiR9uxtpo0bQp8i+McDrMCFqHyblv6XbCNQ=="
|
||||||
|
},
|
||||||
"node_modules/timm": {
|
"node_modules/timm": {
|
||||||
"version": "1.7.1",
|
"version": "1.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz",
|
||||||
@ -16251,6 +16450,11 @@
|
|||||||
"punycode": "^2.1.0"
|
"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=="
|
||||||
|
},
|
||||||
"node_modules/url-parse": {
|
"node_modules/url-parse": {
|
||||||
"version": "1.5.10",
|
"version": "1.5.10",
|
||||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||||
@ -16838,8 +17042,7 @@
|
|||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "17.7.2",
|
"version": "17.7.2",
|
||||||
|
@ -77,6 +77,7 @@
|
|||||||
"escape-html": "1.0.3",
|
"escape-html": "1.0.3",
|
||||||
"eslint": "^9.9.0",
|
"eslint": "^9.9.0",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
"express-openid-connect": "^2.17.1",
|
||||||
"express-partial-content": "1.0.2",
|
"express-partial-content": "1.0.2",
|
||||||
"express-rate-limit": "^7.3.1",
|
"express-rate-limit": "^7.3.1",
|
||||||
"express-session": "1.18.0",
|
"express-session": "1.18.0",
|
||||||
@ -128,6 +129,7 @@
|
|||||||
"split.js": "1.6.5",
|
"split.js": "1.6.5",
|
||||||
"stream-throttle": "0.1.3",
|
"stream-throttle": "0.1.3",
|
||||||
"striptags": "3.2.0",
|
"striptags": "3.2.0",
|
||||||
|
"time2fa": "^1.3.0",
|
||||||
"tmp": "0.2.3",
|
"tmp": "0.2.3",
|
||||||
"tree-kill": "1.2.2",
|
"tree-kill": "1.2.2",
|
||||||
"turndown": "^7.2.0",
|
"turndown": "^7.2.0",
|
||||||
|
@ -14,6 +14,8 @@ import custom from "./routes/custom.js";
|
|||||||
import error_handlers from "./routes/error_handlers.js";
|
import error_handlers from "./routes/error_handlers.js";
|
||||||
import { startScheduledCleanup } from "./services/erase.js";
|
import { startScheduledCleanup } from "./services/erase.js";
|
||||||
import sql_init from "./services/sql_init.js";
|
import sql_init from "./services/sql_init.js";
|
||||||
|
import oidc from "express-openid-connect";
|
||||||
|
import openID from "./services/open_id.js";
|
||||||
|
|
||||||
await import('./services/handlers.js');
|
await import('./services/handlers.js');
|
||||||
await import('./becca/becca_loader.js');
|
await import('./becca/becca_loader.js');
|
||||||
@ -50,6 +52,9 @@ app.use(`/robots.txt`, express.static(path.join(scriptDir, 'public/robots.txt'))
|
|||||||
app.use(sessionParser);
|
app.use(sessionParser);
|
||||||
app.use(favicon(`${scriptDir}/../images/app-icons/icon.ico`));
|
app.use(favicon(`${scriptDir}/../images/app-icons/icon.ico`));
|
||||||
|
|
||||||
|
if (openID.checkOpenIDRequirements())
|
||||||
|
app.use(oidc.auth(openID.generateOAuthConfig()));
|
||||||
|
|
||||||
assets.register(app);
|
assets.register(app);
|
||||||
routes.register(app);
|
routes.register(app);
|
||||||
custom.register(app);
|
custom.register(app);
|
||||||
|
9
src/errors/open_id_error.ts
Normal file
9
src/errors/open_id_error.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
class OpenIDError {
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
constructor(message: string) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OpenIDError;
|
@ -33,6 +33,7 @@ import BackendLogWidget from "./content/backend_log.js";
|
|||||||
import AttachmentErasureTimeoutOptions from "./options/other/attachment_erasure_timeout.js";
|
import AttachmentErasureTimeoutOptions from "./options/other/attachment_erasure_timeout.js";
|
||||||
import RibbonOptions from "./options/appearance/ribbon.js";
|
import RibbonOptions from "./options/appearance/ribbon.js";
|
||||||
import LocalizationOptions from "./options/appearance/i18n.js";
|
import LocalizationOptions from "./options/appearance/i18n.js";
|
||||||
|
import MultiFactorAuthenticationOptions from './options/multi_factor_authentication.js';
|
||||||
|
|
||||||
const TPL = `<div class="note-detail-content-widget note-detail-printable">
|
const TPL = `<div class="note-detail-content-widget note-detail-printable">
|
||||||
<style>
|
<style>
|
||||||
@ -79,6 +80,7 @@ const CONTENT_WIDGETS = {
|
|||||||
_optionsImages: [ ImageOptions ],
|
_optionsImages: [ ImageOptions ],
|
||||||
_optionsSpellcheck: [ SpellcheckOptions ],
|
_optionsSpellcheck: [ SpellcheckOptions ],
|
||||||
_optionsPassword: [ PasswordOptions ],
|
_optionsPassword: [ PasswordOptions ],
|
||||||
|
_optionsMFA: [ MultiFactorAuthenticationOptions ],
|
||||||
_optionsEtapi: [ EtapiOptions ],
|
_optionsEtapi: [ EtapiOptions ],
|
||||||
_optionsBackup: [ BackupOptions ],
|
_optionsBackup: [ BackupOptions ],
|
||||||
_optionsSync: [ SyncOptions ],
|
_optionsSync: [ SyncOptions ],
|
||||||
|
@ -0,0 +1,297 @@
|
|||||||
|
import server from "../../../services/server.js";
|
||||||
|
import toastService from "../../../services/toast.js";
|
||||||
|
import OptionsWidget from "./options_widget.js";
|
||||||
|
|
||||||
|
const TPL = `
|
||||||
|
<div class="options-section">
|
||||||
|
<h2 class=""><b>What is Multi-Factor Authentication?</b></h2>
|
||||||
|
<div class="">
|
||||||
|
<i>
|
||||||
|
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 can'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.</i>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div>
|
||||||
|
<h3><b>OAuth/OpenID</b></h3>
|
||||||
|
<span><i>OpenID is a standardized way to let you log into websites using an account from another service, like Google, to verify your identity.</i></span>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<b>Enable OAuth/OpenID</b>
|
||||||
|
</label>
|
||||||
|
<input type="checkbox" class="oauth-enabled-checkbox" disabled="true" />
|
||||||
|
<span class="env-oauth-enabled" "alert alert-warning" role="alert" style="font-weight: bold; color: red !important;" > </span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span> <b>Token status: </b></span><span class="token-status"> Needs login! </span><span><b> User status: </b></span><span class="user-status"> No user saved!</span>
|
||||||
|
<br>
|
||||||
|
<button class="oauth-login-button" onclick="location.href='/authenticate'" > Login to configured OAuth/OpenID service </button>
|
||||||
|
<button class="save-user-button" > Save User </button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<h3><b>Time-based One-Time Password</b></h3>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<b>Enable TOTP</b>
|
||||||
|
</label>
|
||||||
|
<input type="checkbox" class="totp-enabled" />
|
||||||
|
<span class="env-totp-enabled" "alert alert-warning" role="alert" style="font-weight: bold; color: red !important;" > </span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span><i>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.</i></span>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<h4> Generate TOTP Secret </h4>
|
||||||
|
<div>
|
||||||
|
<span class="totp-secret" > TOTP Secret Key </span>
|
||||||
|
<br>
|
||||||
|
<button class="regenerate-totp" disabled="true"> Regenerate TOTP Secret </button>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<h4> Single Sign-on Recovery Keys </h4>
|
||||||
|
<div>
|
||||||
|
<span ><i>Single sign-on recovery keys are used to login in the event you cannot access your Authenticator codes. Keep them somewhere safe and secure. </i></span>
|
||||||
|
<br><br>
|
||||||
|
<span class="alert alert-warning" role="alert" style="font-weight: bold; color: red !important;">After a recovery key is used it cannot be used again.</span>
|
||||||
|
<br><br>
|
||||||
|
<table style="border: 0px solid white">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="key_0">Recover Key 1</td>
|
||||||
|
<td style="width: 20px" />
|
||||||
|
<td class="key_1">Recover Key 2</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="key_2">Recover Key 3</td>
|
||||||
|
<td />
|
||||||
|
<td class="key_3">Recover Key 4</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="key_4">Recover Key 5</td>
|
||||||
|
<td />
|
||||||
|
<td class="key_5">Recover Key 6</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="key_6">Recover Key 7</td>
|
||||||
|
<td />
|
||||||
|
<td class="key_7">Recover Key 8</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<br>
|
||||||
|
<button class="generate-recovery-code" disabled="true"> Generate Recovery Keys </button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default class MultiFactorAuthenticationOptions extends OptionsWidget {
|
||||||
|
doRender() {
|
||||||
|
this.$widget = $(TPL);
|
||||||
|
|
||||||
|
this.$regenerateTotpButton = this.$widget.find(".regenerate-totp");
|
||||||
|
this.$totpDetails = this.$widget.find(".totp-details");
|
||||||
|
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.$saveUserButton = this.$widget.find(".save-user-button");
|
||||||
|
this.$oauthLoginButton = this.$widget.find(".oauth-login-button");
|
||||||
|
this.$tokenStatus = this.$widget.find(".token-status");
|
||||||
|
this.$userStatus = this.$widget.find(".user-status");
|
||||||
|
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.$totpEnabled.on("change", async () => {
|
||||||
|
this.updateSecret();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$oAuthEnabledCheckbox.on("change", async () => {
|
||||||
|
this.updateOAuthStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$generateRecoveryCodeButton.on("click", async () => {
|
||||||
|
this.setRecoveryKeys();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$regenerateTotpButton.on("click", async () => {
|
||||||
|
this.generateKey();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$saveUserButton.on("click", (async) => {
|
||||||
|
server
|
||||||
|
.get("oauth/authenticate")
|
||||||
|
.then((result) => {
|
||||||
|
console.log(result.message);
|
||||||
|
toastService.showMessage(result.message);
|
||||||
|
})
|
||||||
|
.catch((result) => {
|
||||||
|
console.error(result.message);
|
||||||
|
toastService.showError(result.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$protectedSessionTimeout = this.$widget.find(
|
||||||
|
".protected-session-timeout-in-seconds"
|
||||||
|
);
|
||||||
|
this.$protectedSessionTimeout.on("change", () =>
|
||||||
|
this.updateOption(
|
||||||
|
"protectedSessionTimeout",
|
||||||
|
this.$protectedSessionTimeout.val()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.displayRecoveryKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSecret() {
|
||||||
|
if (this.$totpEnabled.prop("checked")) {
|
||||||
|
server.post("totp/enable");
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
server.post("totp/disable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOAuthStatus() {
|
||||||
|
if (this.$oAuthEnabledCheckbox.prop("checked")){
|
||||||
|
server.post("oauth/enable");
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
server.post("oauth/disable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// TODO: Rework the logic since I've changed how OAuth works
|
||||||
|
|
||||||
|
// server.get("oauth/status").then((result) => {
|
||||||
|
// if (result.enabled) {
|
||||||
|
// if (result.success)
|
||||||
|
// this.$oAuthEnabledCheckbox.prop("checked", result.message);
|
||||||
|
|
||||||
|
// this.$oauthLoginButton.prop("disabled", !result.message);
|
||||||
|
// this.$saveUserButton.prop("disabled", !result.message);
|
||||||
|
|
||||||
|
// if (result.message) {
|
||||||
|
// this.$oauthLoginButton.prop("disabled", false);
|
||||||
|
// this.$saveUserButton.prop("disabled", false);
|
||||||
|
// server.get("oauth/validate").then((result) => {
|
||||||
|
// if (result.success) {
|
||||||
|
// this.$tokenStatus.text("Logged in!");
|
||||||
|
|
||||||
|
// if (result.user) {
|
||||||
|
// this.$userStatus.text("User saved!");
|
||||||
|
// } else {
|
||||||
|
// this.$saveUserButton.prop("disabled", false);
|
||||||
|
// this.$userStatus.text("User not saved");
|
||||||
|
// }
|
||||||
|
// } else this.$tokenStatus.text("Not logged in!");
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// this.$oAuthEnabledCheckbox.prop("checked", false);
|
||||||
|
// this.$oauthLoginButton.prop("disabled", true);
|
||||||
|
// this.$saveUserButton.prop("disabled", true);
|
||||||
|
// this.$oAuthEnabledCheckbox.prop("disabled", true);
|
||||||
|
|
||||||
|
// this.$envEnabledOAuth.text(
|
||||||
|
// "OAuth can only be enabled with environment variables. REQUIRES RESTART"
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
server.get("totp/status").then((result) => {
|
||||||
|
if (result.enabled)
|
||||||
|
if (result.success) {
|
||||||
|
this.$totpEnabled.prop("checked", result.message);
|
||||||
|
this.$totpSecretInput.prop("disabled", !result.message);
|
||||||
|
this.$totpSecret.prop("disapbled", !result.message);
|
||||||
|
this.$regenerateTotpButton.prop("disabled", !result.message);
|
||||||
|
this.$authenticatorCode.prop("disabled", !result.message);
|
||||||
|
this.$generateRecoveryCodeButton.prop("disabled", !result.message);
|
||||||
|
} else {
|
||||||
|
toastService.showError(result.message);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.$totpEnabled.prop("checked", false);
|
||||||
|
this.$totpEnabled.prop("disabled", true);
|
||||||
|
this.$totpSecretInput.prop("disabled", true);
|
||||||
|
this.$totpSecret.prop("disapbled", true);
|
||||||
|
this.$regenerateTotpButton.prop("disabled", true);
|
||||||
|
this.$authenticatorCode.prop("disabled", true);
|
||||||
|
this.$generateRecoveryCodeButton.prop("disabled", true);
|
||||||
|
|
||||||
|
this.$envEnabledTOTP.text(
|
||||||
|
"TOTP_ENABLED is not set in environment variable. 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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
51
src/routes/api/recovery_codes.ts
Normal file
51
src/routes/api/recovery_codes.ts
Normal file
@ -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
|
||||||
|
};
|
49
src/routes/api/totp.ts
Normal file
49
src/routes/api/totp.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import options from '../../services/options.js';
|
||||||
|
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 = options.getOptionBool('totpEnabled');
|
||||||
|
return {success: 'true', message: totpEnabled, enabled: getTotpEnabled()};
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableTOTP() {
|
||||||
|
if (!getTotpEnabled()) {
|
||||||
|
return {success: 'false'};
|
||||||
|
}
|
||||||
|
|
||||||
|
options.setOption('totpEnabled', true);
|
||||||
|
options.setOption('oAuthEnabled', false);
|
||||||
|
return {success: 'true'};
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableTOTP() {
|
||||||
|
options.setOption('totpEnabled', false);
|
||||||
|
return {success: true};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSecret() {
|
||||||
|
return process.env.TOTP_SECRET;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
generateSecret: generateTOTPSecret,
|
||||||
|
getTOTPStatus,
|
||||||
|
enableTOTP,
|
||||||
|
disableTOTP,
|
||||||
|
getSecret
|
||||||
|
};
|
@ -10,16 +10,26 @@ import appPath from "../services/app_path.js";
|
|||||||
import ValidationError from "../errors/validation_error.js";
|
import ValidationError from "../errors/validation_error.js";
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { AppRequest } from './route-interface.js';
|
import { AppRequest } from './route-interface.js';
|
||||||
|
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) {
|
function loginPage(req: Request, res: Response) {
|
||||||
|
if (open_id.isOpenIDEnabled()) {
|
||||||
|
res.redirect('/authenticate');
|
||||||
|
} else {
|
||||||
res.render('login', {
|
res.render('login', {
|
||||||
failedAuth: false,
|
failedAuth: false,
|
||||||
|
totpEnabled: optionService.getOptionBool('totpEnabled') && totp.checkForTotSecret(),
|
||||||
assetPath: assetPath,
|
assetPath: assetPath,
|
||||||
appPath: appPath
|
appPath: appPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setPasswordPage(req: Request, res: Response) {
|
function setPasswordPage(req: Request, res: Response) {
|
||||||
res.render('set_password', {
|
res.render('set_password', {
|
||||||
error: false,
|
error: false,
|
||||||
assetPath: assetPath,
|
assetPath: assetPath,
|
||||||
@ -59,8 +69,19 @@ function setPassword(req: Request, res: Response) {
|
|||||||
|
|
||||||
function login(req: AppRequest, res: Response) {
|
function login(req: AppRequest, res: Response) {
|
||||||
const guessedPassword = req.body.password;
|
const guessedPassword = req.body.password;
|
||||||
|
const guessedTotp = req.body.token;
|
||||||
|
|
||||||
if (verifyPassword(guessedPassword)) {
|
if (verifyPassword(guessedPassword)) {
|
||||||
|
if (!verifyPassword(guessedPassword)) {
|
||||||
|
sendLoginError(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optionService.getOptionBool('totpEnabled') && totp.checkForTotSecret())
|
||||||
|
if (!verifyTOTP(guessedTotp)) {
|
||||||
|
sendLoginError(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const rememberMe = req.body.rememberMe;
|
const rememberMe = req.body.rememberMe;
|
||||||
|
|
||||||
req.session.regenerate(() => {
|
req.session.regenerate(() => {
|
||||||
@ -85,6 +106,14 @@ function login(req: AppRequest, res: Response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function verifyTOTP(guessedToken: string) {
|
||||||
|
if (totp.validateTOTP(guessedToken)) return true;
|
||||||
|
|
||||||
|
const recoveryCodeValidates = recoveryCodeService.verifyRecoveryCode(guessedToken);
|
||||||
|
|
||||||
|
return recoveryCodeValidates;
|
||||||
|
}
|
||||||
|
|
||||||
function verifyPassword(guessedPassword: string) {
|
function verifyPassword(guessedPassword: string) {
|
||||||
const hashed_password = utils.fromBase64(optionService.getOption('passwordVerificationHash'));
|
const hashed_password = utils.fromBase64(optionService.getOption('passwordVerificationHash'));
|
||||||
|
|
||||||
@ -93,11 +122,23 @@ function verifyPassword(guessedPassword: string) {
|
|||||||
return guess_hashed.equals(hashed_password);
|
return guess_hashed.equals(hashed_password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendLoginError(req: AppRequest, res: Response) {
|
||||||
|
// note that logged IP address is usually meaningless since the traffic should come from a reverse proxy
|
||||||
|
log.info(`WARNING: Wrong password or TOTP from ${req.ip}, rejecting.`);
|
||||||
|
|
||||||
|
res.status(401).render('login', {
|
||||||
|
failedAuth: true,
|
||||||
|
totpEnabled: optionService.getOption('totpEnabled') && totp.checkForTotSecret(),
|
||||||
|
assetPath: assetPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function logout(req: AppRequest, res: Response) {
|
function logout(req: AppRequest, res: Response) {
|
||||||
req.session.regenerate(() => {
|
req.session.regenerate(() => {
|
||||||
req.session.loggedIn = false;
|
req.session.loggedIn = false;
|
||||||
|
if (openIDService.isOpenIDEnabled() && openIDEncryption.isSubjectIdentifierSaved()) {
|
||||||
res.redirect('login');
|
res.oidc.logout({ returnTo: '/authenticate' });
|
||||||
|
} else res.redirect('login');
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,9 @@ import log from "../services/log.js";
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import auth from "../services/auth.js";
|
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 cls from "../services/cls.js";
|
||||||
import sql from "../services/sql.js";
|
import sql from "../services/sql.js";
|
||||||
import entityChangesService from "../services/entity_changes.js";
|
import entityChangesService from "../services/entity_changes.js";
|
||||||
@ -72,6 +75,7 @@ import etapiSpecRoute from "../etapi/spec.js";
|
|||||||
import etapiBackupRoute from "../etapi/backup.js";
|
import etapiBackupRoute from "../etapi/backup.js";
|
||||||
import { AppRequest, AppRequestHandler } from './route-interface.js';
|
import { AppRequest, AppRequestHandler } from './route-interface.js';
|
||||||
|
|
||||||
|
|
||||||
const csrfMiddleware = csurf({
|
const csrfMiddleware = csurf({
|
||||||
cookie: {
|
cookie: {
|
||||||
path: "" // empty, so cookie is valid only for the current path
|
path: "" // empty, so cookie is valid only for the current path
|
||||||
@ -117,6 +121,22 @@ function register(app: express.Application) {
|
|||||||
route(PST, '/set-password', [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPassword);
|
route(PST, '/set-password', [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPassword);
|
||||||
route(GET, '/setup', [], setupRoute.setupPage);
|
route(GET, '/setup', [], setupRoute.setupPage);
|
||||||
|
|
||||||
|
|
||||||
|
apiRoute(GET, '/api/totp/generate', totp.generateSecret);
|
||||||
|
apiRoute(GET, '/api/totp/status', totp.getTOTPStatus);
|
||||||
|
apiRoute(PST, '/api/totp/enable', totp.enableTOTP);
|
||||||
|
apiRoute(PST, '/api/totp/disable', totp.disableTOTP);
|
||||||
|
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(GET, '/api/tree', treeApiRoute.getTree);
|
||||||
apiRoute(PST, '/api/tree/load', treeApiRoute.load);
|
apiRoute(PST, '/api/tree/load', treeApiRoute.load);
|
||||||
|
|
||||||
|
@ -9,17 +9,33 @@ import config from "./config.js";
|
|||||||
import passwordService from "./encryption/password.js";
|
import passwordService from "./encryption/password.js";
|
||||||
import type { NextFunction, Request, Response } from 'express';
|
import type { NextFunction, Request, Response } from 'express';
|
||||||
import { AppRequest } from '../routes/route-interface.js';
|
import { AppRequest } from '../routes/route-interface.js';
|
||||||
|
import openID from './open_id.js';
|
||||||
|
import sql from './sql.js';
|
||||||
|
import open_id_encryption from './encryption/open_id_encryption.js';
|
||||||
|
|
||||||
const noAuthentication = config.General && config.General.noAuthentication === true;
|
const noAuthentication = config.General && config.General.noAuthentication === true;
|
||||||
|
|
||||||
function checkAuth(req: AppRequest, res: Response, next: NextFunction) {
|
function checkAuth(req: AppRequest, res: Response, next: NextFunction) {
|
||||||
if (!sqlInit.isDbInitialized()) {
|
if (!sqlInit.isDbInitialized()) {
|
||||||
res.redirect("setup");
|
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) {
|
} else if (
|
||||||
res.redirect("login");
|
!req.session.loggedIn &&
|
||||||
}
|
!utils.isElectron() &&
|
||||||
else {
|
!noAuthentication
|
||||||
|
) {
|
||||||
|
res.redirect('login');
|
||||||
|
} else {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import optionService from "../options.js";
|
import optionService from "../options.js";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
import utils from "../utils.js";
|
||||||
|
import sql from "../sql.js";
|
||||||
|
|
||||||
function getVerificationHash(password: crypto.BinaryLike) {
|
function getVerificationHash(password: crypto.BinaryLike) {
|
||||||
const salt = optionService.getOption('passwordVerificationSalt');
|
const salt = optionService.getOption('passwordVerificationSalt');
|
||||||
@ -22,7 +24,50 @@ function getScryptHash(password: crypto.BinaryLike, salt: crypto.BinaryLike) {
|
|||||||
return hashed;
|
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
|
||||||
|
) {
|
||||||
|
// const salt = optionService.getOption("subjectIdentifierDerivedKeySalt");
|
||||||
|
|
||||||
|
// const salt = sql.getValue("SELECT salt FROM user_data");
|
||||||
|
// if (salt === undefined || salt === null) return undefined;
|
||||||
|
|
||||||
|
return getScryptHash(subjectIdentifer, salt);
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getVerificationHash,
|
getVerificationHash,
|
||||||
getPasswordDerivedKey
|
getPasswordDerivedKey,
|
||||||
|
getSubjectIdentifierVerificationHash,
|
||||||
|
getSubjectIdentifierDerivedKey,
|
||||||
|
createSubjectIdentifierDerivedKey
|
||||||
};
|
};
|
||||||
|
163
src/services/encryption/open_id_encryption.ts
Normal file
163
src/services/encryption/open_id_encryption.ts
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import optionService from "../options.js";
|
||||||
|
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";
|
||||||
|
|
||||||
|
function saveSubjectIdentifier(subjectIdentifier: string) {
|
||||||
|
if (isUserSaved()) return false;
|
||||||
|
|
||||||
|
// Allows setup with existing instances of trilium
|
||||||
|
sql.execute(`
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_data"
|
||||||
|
(
|
||||||
|
tmpID INT,
|
||||||
|
userIDEcnryptedDataKey TEXT,
|
||||||
|
userIDVerificationHash TEXT,
|
||||||
|
salt TEXT,
|
||||||
|
derivedKey TEXT,
|
||||||
|
isSetup TEXT DEFAULT "false",
|
||||||
|
UNIQUE (tmpID),
|
||||||
|
PRIMARY KEY (tmpID)
|
||||||
|
);`);
|
||||||
|
|
||||||
|
const verificationSalt = utils.randomSecureToken(32);
|
||||||
|
const derivedKeySalt = utils.randomSecureToken(32);
|
||||||
|
|
||||||
|
const verificationHash = myScryptService.getSubjectIdentifierVerificationHash(
|
||||||
|
subjectIdentifier,
|
||||||
|
verificationSalt
|
||||||
|
);
|
||||||
|
if (verificationHash === undefined) {
|
||||||
|
console.log("Verification hash undefined!");
|
||||||
|
return 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",
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Saved data: " + data);
|
||||||
|
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<string>("SELECT isSetup FROM user_data;");
|
||||||
|
return isSaved === "true" ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyOpenIDSubjectIdentifier(subjectIdentifier: string) {
|
||||||
|
if (!sqlInit.isDbInitialized()) {
|
||||||
|
console.log("Database not initialized!");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isUserSaved()) {
|
||||||
|
console.log("DATABASE NOT SETUP");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
console.log("Subject Identifier: " + subjectIdentifier);
|
||||||
|
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) {
|
||||||
|
console.log("Subject Identifier: " + subjectIdentifier);
|
||||||
|
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,
|
||||||
|
saveSubjectIdentifier,
|
||||||
|
isSubjectIdentifierSaved,
|
||||||
|
};
|
90
src/services/encryption/recovery_codes.ts
Normal file
90
src/services/encryption/recovery_codes.ts
Normal file
@ -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
|
||||||
|
};
|
@ -249,6 +249,7 @@ const HIDDEN_SUBTREE_DEFINITION: Item = {
|
|||||||
{ id: '_optionsImages', title: 'Images', type: 'contentWidget', icon: 'bx-image' },
|
{ id: '_optionsImages', title: 'Images', type: 'contentWidget', icon: 'bx-image' },
|
||||||
{ id: '_optionsSpellcheck', title: 'Spellcheck', type: 'contentWidget', icon: 'bx-check-double' },
|
{ id: '_optionsSpellcheck', title: 'Spellcheck', type: 'contentWidget', icon: 'bx-check-double' },
|
||||||
{ id: '_optionsPassword', title: 'Password', type: 'contentWidget', icon: 'bx-lock' },
|
{ id: '_optionsPassword', title: 'Password', type: 'contentWidget', icon: 'bx-lock' },
|
||||||
|
{ id: '_optionsMFA', title: 'MFA', type: 'contentWidget', icon: 'bx-lock'},
|
||||||
{ id: '_optionsEtapi', title: 'ETAPI', type: 'contentWidget', icon: 'bx-extension' },
|
{ id: '_optionsEtapi', title: 'ETAPI', type: 'contentWidget', icon: 'bx-extension' },
|
||||||
{ id: '_optionsBackup', title: 'Backup', type: 'contentWidget', icon: 'bx-data' },
|
{ id: '_optionsBackup', title: 'Backup', type: 'contentWidget', icon: 'bx-data' },
|
||||||
{ id: '_optionsSync', title: 'Sync', type: 'contentWidget', icon: 'bx-wifi' },
|
{ id: '_optionsSync', title: 'Sync', type: 'contentWidget', icon: 'bx-wifi' },
|
||||||
|
136
src/services/open_id.ts
Normal file
136
src/services/open_id.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
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 dbf = sql.getValue<string>("SELECT isSetup FROM user_data;");
|
||||||
|
return dbf === "true" ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkOpenIDRequirements() {
|
||||||
|
if (process.env.OAUTH_ENABLED === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (process.env.OAUTH_ENABLED.toLocaleLowerCase() !== "true") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.ISSUER_BASE_URL === undefined) {
|
||||||
|
throw new OpenIDError("ISSUER_BASE_URL is undefined in .env!");
|
||||||
|
}
|
||||||
|
if (process.env.SECRET === undefined) {
|
||||||
|
throw new OpenIDError("SECRET is undefined in .env!");
|
||||||
|
}
|
||||||
|
if (process.env.AUTH_0_LOGOUT === undefined) {
|
||||||
|
throw new OpenIDError("AUTH_0_LOGOUT is undefined in .env!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOAuthStatus() {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: checkOpenIDRequirements(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 checkAuth0Logout() {
|
||||||
|
if (process.env.AUTH_0_LOGOUT === undefined) return false;
|
||||||
|
if (process.env.AUTH_0_LOGOUT.toLocaleLowerCase() === "true") return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateOAuthConfig() {
|
||||||
|
const authRoutes = {
|
||||||
|
callback: "/callback",
|
||||||
|
login: "/authenticate",
|
||||||
|
postLogoutRedirect: "/login",
|
||||||
|
logout: "/logout",
|
||||||
|
};
|
||||||
|
|
||||||
|
const logoutParams = {
|
||||||
|
// end_session_endpoint: "/end-session/",
|
||||||
|
};
|
||||||
|
|
||||||
|
const authConfig = {
|
||||||
|
authRequired: true,
|
||||||
|
auth0Logout: checkAuth0Logout(),
|
||||||
|
baseURL: process.env.BASE_URL,
|
||||||
|
clientID: process.env.CLIENT_ID,
|
||||||
|
issuerBaseURL: process.env.ISSUER_BASE_URL,
|
||||||
|
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.saveSubjectIdentifier(req.oidc.user.sub.toString());
|
||||||
|
|
||||||
|
return session;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return authConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
generateOAuthConfig,
|
||||||
|
getOAuthStatus,
|
||||||
|
isOpenIDEnabled,
|
||||||
|
checkOpenIDRequirements,
|
||||||
|
isTokenValid,
|
||||||
|
isUserSaved,
|
||||||
|
};
|
@ -95,6 +95,10 @@ const defaultOptions: DefaultOption[] = [
|
|||||||
{ name: 'customSearchEngineUrl', value: 'https://duckduckgo.com/?q={keyword}', isSynced: true },
|
{ name: 'customSearchEngineUrl', value: 'https://duckduckgo.com/?q={keyword}', isSynced: true },
|
||||||
{ name: 'promotedAttributesOpenInRibbon', value: 'true', isSynced: true },
|
{ name: 'promotedAttributesOpenInRibbon', value: 'true', isSynced: true },
|
||||||
{ name: 'editedNotesOpenInRibbon', 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
|
// Internationalization
|
||||||
{ name: 'locale', value: 'en', isSynced: true },
|
{ name: 'locale', value: 'en', isSynced: true },
|
||||||
|
32
src/services/totp.ts
Normal file
32
src/services/totp.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import {Totp} from 'time2fa';
|
||||||
|
|
||||||
|
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 {
|
||||||
|
getTotpSecret,
|
||||||
|
checkForTotSecret,
|
||||||
|
validateTOTP
|
||||||
|
};
|
@ -25,6 +25,15 @@
|
|||||||
<input id="password" name="password" placeholder="" class="form-control" type="password">
|
<input id="password" name="password" placeholder="" class="form-control" type="password">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<% if( totpEnabled ) { %>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="otp-token">OTP Token</label>
|
||||||
|
<div class="controls">
|
||||||
|
<input id="token" name="token" placeholder="" class="form-control" type="text"
|
||||||
|
required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user