Ported from branch OIDC

This commit is contained in:
chesspro13 2024-09-07 10:21:41 -07:00
parent 1c8cc36402
commit 9c748f326a
No known key found for this signature in database
GPG Key ID: 5FEAE94D298066E5
22 changed files with 1221 additions and 24 deletions

4
.gitignore vendored
View File

@ -31,4 +31,6 @@ images/app-icons/mac/*.png
/playwright-report/ /playwright-report/
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
/playwright/.auth/ /playwright/.auth/
.env

View File

@ -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
View 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
View File

@ -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",

View File

@ -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",

View File

@ -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);

View File

@ -0,0 +1,9 @@
class OpenIDError {
message: string;
constructor(message: string) {
this.message = message;
}
}
export default OpenIDError;

View File

@ -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 ],

View File

@ -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");
});
}
}

View 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
View 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
};

View File

@ -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) {
res.render('login', { if (open_id.isOpenIDEnabled()) {
res.redirect('/authenticate');
} else {
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');
}); });
} }

View File

@ -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);

View File

@ -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()) {
else if (!req.session.loggedIn && !utils.isElectron() && !noAuthentication) { if (
res.redirect("login"); req.oidc.isAuthenticated() &&
} open_id_encryption.verifyOpenIDSubjectIdentifier(req.oidc.user?.sub)
else { ) {
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(); next();
} }
} }

View File

@ -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
}; };

View 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,
};

View 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
};

View File

@ -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
View 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,
};

View File

@ -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
View 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
};

View File

@ -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>