From e1ddb95250357a4f1eb63386a71ce24ba00562e1 Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Fri, 24 Mar 2023 18:21:25 +0100 Subject: [PATCH 01/39] Inital passportjs integration --- package-lock.json | 433 +++++++++++++++++++++- package.json | 7 +- server/Auth.js | 346 ++++++++--------- server/Server.js | 38 +- server/controllers/UserController.js | 4 +- server/objects/settings/ServerSettings.js | 42 ++- 6 files changed, 695 insertions(+), 175 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8edcbc01..efddfd17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,14 @@ "dependencies": { "axios": "^0.27.2", "express": "^4.17.1", + "express-session": "^1.17.3", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", "node-tone": "^1.0.1", + "passport": "^0.6.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "socket.io": "^4.5.4", "xml2js": "^0.4.23" }, @@ -111,6 +116,14 @@ "node": "^4.5.0 || >= 5.9" } }, + "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/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -165,6 +178,11 @@ "node": ">=8" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -357,6 +375,14 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -492,6 +518,32 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "dependencies": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -754,6 +806,75 @@ "node": ">=0.12.0" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "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/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -902,6 +1023,11 @@ "node": ">=0.10.0" } }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -929,6 +1055,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -937,11 +1071,91 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", + "integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -986,6 +1200,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1278,6 +1500,22 @@ "node": ">= 0.6" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -1347,6 +1585,11 @@ "engines": { "node": ">=4.0" } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } }, "dependencies": { @@ -1428,6 +1671,11 @@ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1472,6 +1720,11 @@ "fill-range": "^7.0.1" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1604,6 +1857,14 @@ "domhandler": "^5.0.1" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1709,6 +1970,28 @@ "vary": "~1.1.2" } }, + "express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "requires": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + } + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1889,6 +2172,64 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "requires": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "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==", + "requires": { + "yallist": "^4.0.0" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1996,6 +2337,11 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2014,16 +2360,78 @@ "ee-first": "1.1.1" } }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + } + }, + "passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "requires": { + "passport-oauth2": "1.x.x" + } + }, + "passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "requires": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "requires": { + "passport-strategy": "1.x.x" + } + }, + "passport-oauth2": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", + "integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==", + "requires": { + "base64url": "3.x.x", + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2053,6 +2461,11 @@ "side-channel": "^1.0.4" } }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2272,6 +2685,19 @@ "mime-types": "~2.1.24" } }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, + "uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -2312,6 +2738,11 @@ "version": "11.0.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index a8b1c69b..0cf19f48 100644 --- a/package.json +++ b/package.json @@ -32,13 +32,18 @@ "dependencies": { "axios": "^0.27.2", "express": "^4.17.1", + "express-session": "^1.17.3", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", "node-tone": "^1.0.1", + "passport": "^0.6.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "socket.io": "^4.5.4", "xml2js": "^0.4.23" }, "devDependencies": { "nodemon": "^2.0.20" } -} \ No newline at end of file +} diff --git a/server/Auth.js b/server/Auth.js index 2bca48d2..4cb40da6 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -1,43 +1,144 @@ +const passport = require('passport') const bcrypt = require('./libs/bcryptjs') const jwt = require('./libs/jsonwebtoken') -const requestIp = require('./libs/requestIp') -const Logger = require('./Logger') +const LocalStrategy = require('passport-local') +const JwtStrategy = require('passport-jwt').Strategy; +const ExtractJwt = require('passport-jwt').ExtractJwt; +const GoogleStrategy = require('passport-google-oauth20').Strategy; +const User = require('./objects/user/User.js') +/** + * @class Class for handling all the authentication related functionality. + */ class Auth { + constructor(db) { this.db = db - - this.user = null } - get username() { - return this.user ? this.user.username : 'nobody' + /** + * Inializes all passportjs stragegies and other passportjs ralated initialization. + */ + initPassportJs() { + // Check if we should load the local strategy + if (global.ServerSettings.authActiveAuthMethods.includes("local")) { + passport.use(new LocalStrategy(this.localAuthCheckUserPw.bind(this))) + } + // Check if we should load the google-oauth20 strategy + if (global.ServerSettings.authActiveAuthMethods.includes("google-oauth20")) { + passport.use(new GoogleStrategy({ + clientID: global.ServerSettings.authGoogleOauth20ClientID, + clientSecret: global.ServerSettings.authGoogleOauth20ClientSecret, + callbackURL: global.ServerSettings.authGoogleOauth20CallbackURL + }, function (accessToken, refreshToken, profile, done) { + // TODO: what to use as username + // TODO: do we want to create the users which does not exist? + return done(null, { username: profile.emails[0].value }) + })) + } + + // Load the JwtStrategy (always) -> for bearer token auth + passport.use(new JwtStrategy({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: global.ServerSettings.tokenSecret + }, this.jwtAuthCheck.bind(this))) + + // define how to seralize a user (to be put into the session) + passport.serializeUser(function (user, cb) { + process.nextTick(function () { + // only store username and id to session + // TODO: do we want to store more info in the session? + return cb(null, { + "username": user.username, + "id": user.id, + }); + }); + }); + + // define how to deseralize a user (use the username to get it from the database) + passport.deserializeUser(function (user, cb) { + process.nextTick(function () { + parsedUserInfo = JSON.parse(user) + // TODO: do the matching on username or better on id? + var dbUser = this.db.users.find(u => u.username.toLowerCase() === parsedUserInfo.username.toLowerCase()) + return cb(null, new User(dbUser)); + }); + }); } - get users() { - return this.db.users + /** + * Creates all (express) routes required for authentication. + * @param {express.Router} router + */ + initAuthRoutes(router) { + // just a route saying "you need to login" where we redirect e.g. after logout + // TODO: replace with a 401? + router.get('/login', function (req, res) { + res.send('please login') + }) + + // Local strategy login route (takes username and password) + router.post('/login', passport.authenticate('local', { + failureRedirect: '/login' + }), + (function (req, res) { + // return the user login response json if the login was successfull + res.json(this.getUserLoginResponsePayload(req.user.username)) + }).bind(this) + ) + + // google-oauth20 strategy login route (this redirects to the google login) + router.get('/auth/google', passport.authenticate('google', { scope: ['email'] })) + + // google-oauth20 strategy callback route (this receives the token from google) + router.get('/auth/google/callback', + passport.authenticate('google', { failureRedirect: '/login' }), + (function (req, res) { + // return the user login response json if the login was successfull + res.json(this.getUserLoginResponsePayload(req.user.username)) + }).bind(this) + ) + + // Logout route + router.get('/logout', function (req, res) { + // TODO: invalidate possible JWTs + req.logout() + res.redirect('/login') + }) } - cors(req, res, next) { - res.header('Access-Control-Allow-Origin', '*') - res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS') - res.header('Access-Control-Allow-Headers', '*') - // TODO: Make sure allowing all headers is not a security concern. It is required for adding custom headers for SSO - // res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding, Range, Authorization") - res.header('Access-Control-Allow-Credentials', true) - if (req.method === 'OPTIONS') { - res.sendStatus(200) - } else { + /** + * middleware to use in express to only allow authenticated users. + * @param {express.Request} req + * @param {express.Response} res + * @param {express.NextFunction} next + */ + isAuthenticated(req, res, next) { + // check if session cookie says that we are authenticated + if (req.isAuthenticated()) { next() + } else { + // try JWT to authenticate + passport.authenticate("jwt")(req, res, next) } } + /** + * Function to generate a jwt token for a given user. + * @param {Object} user + * @returns the token. + */ + generateAccessToken(user) { + return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret); + } + + /** + * Generate a token for each user. + */ async initTokenSecret() { if (process.env.TOKEN_SECRET) { // User can supply their own token secret - Logger.debug(`[Auth] Setting token secret - using user passed in TOKEN_SECRET env var`) this.db.serverSettings.tokenSecret = process.env.TOKEN_SECRET } else { - Logger.debug(`[Auth] Setting token secret - using random bytes`) this.db.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64') } await this.db.updateServerSettings() @@ -46,46 +147,70 @@ class Auth { if (this.db.users.length) { for (const user of this.db.users) { user.token = await this.generateAccessToken({ userId: user.id, username: user.username }) - Logger.warn(`[Auth] User ${user.username} api token has been updated using new token secret`) } await this.db.updateEntities('user', this.db.users) } } - async authMiddleware(req, res, next) { - var token = null + /** + * Checks if the user in the validated jwt_payload really exists and is active. + * @param {Object} jwt_payload + * @param {function} done + */ + jwtAuthCheck(jwt_payload, done) { + var user = this.db.users.find(u => u.username.toLowerCase() === jwt_payload.username.toLowerCase()) - // If using a get request, the token can be passed as a query string - if (req.method === 'GET' && req.query && req.query.token) { - token = req.query.token - } else { - const authHeader = req.headers['authorization'] - token = authHeader && authHeader.split(' ')[1] + if (!user || !user.isActive) { + done(null, null) + return } - - if (token == null) { - Logger.error('Api called without a token', req.path) - return res.sendStatus(401) - } - - var user = await this.verifyToken(token) - if (!user) { - Logger.error('Verify Token User Not Found', token) - return res.sendStatus(404) - } - if (!user.isActive) { - Logger.error('Verify Token User is disabled', token, user.username) - return res.sendStatus(403) - } - req.user = user - next() + done(null, user) + return } + /** + * Checks if a username and passpword touple is valid and the user active. + * @param {string} username + * @param {string} password + * @param {function} done + */ + localAuthCheckUserPw(username, password, done) { + var user = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase()) + + if (!user || !user.isActive) { + done(null, null) + return + } + + // Check passwordless root user + if (user.id === 'root' && (!user.pash || user.pash === '')) { + if (password) { + done(null, null) + return + } + done(null, user) + return + } + + // Check password match + var compare = bcrypt.compareSync(password, user.pash) + if (compare) { + done(null, user) + return + } + done(null, null) + return + } + + /** + * Hashes a password with bcrypt. + * @param {string} password + * @returns {string} hash + */ hashPass(password) { return new Promise((resolve) => { bcrypt.hash(password, 8, (err, hash) => { if (err) { - Logger.error('Hash failed', err) resolve(null) } else { resolve(hash) @@ -94,28 +219,14 @@ class Auth { }) } - generateAccessToken(payload) { - return jwt.sign(payload, global.ServerSettings.tokenSecret); - } - - authenticateUser(token) { - return this.verifyToken(token) - } - - verifyToken(token) { - return new Promise((resolve) => { - jwt.verify(token, global.ServerSettings.tokenSecret, (err, payload) => { - if (!payload || err) { - Logger.error('JWT Verify Token Failed', err) - return resolve(null) - } - const user = this.users.find(u => u.id === payload.userId && u.username === payload.username) - resolve(user || null) - }) - }) - } - - getUserLoginResponsePayload(user) { + /** + * Return the login info payload for a user. + * @param {string} username + * @returns {string} jsonPayload + */ + getUserLoginResponsePayload(username) { + var user = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase()) + user = new User(user) return { user: user.toJSONForBrowser(), userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries), @@ -123,101 +234,6 @@ class Auth { Source: global.Source } } - - async login(req, res) { - const ipAddress = requestIp.getClientIp(req) - var username = (req.body.username || '').toLowerCase() - var password = req.body.password || '' - - var user = this.users.find(u => u.username.toLowerCase() === username) - - if (!user || !user.isActive) { - Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`) - if (req.rateLimit.remaining <= 2) { - Logger.error(`[Auth] Failed login attempt for username ${username} from ip ${ipAddress}. Attempts: ${req.rateLimit.current}`) - return res.status(401).send(`Invalid user or password (${req.rateLimit.remaining === 0 ? '1 attempt remaining' : `${req.rateLimit.remaining + 1} attempts remaining`})`) - } - return res.status(401).send('Invalid user or password') - } - - // Check passwordless root user - if (user.id === 'root' && (!user.pash || user.pash === '')) { - if (password) { - return res.status(401).send('Invalid root password (hint: there is none)') - } else { - return res.json(this.getUserLoginResponsePayload(user)) - } - } - - // Check password match - var compare = await bcrypt.compare(password, user.pash) - if (compare) { - res.json(this.getUserLoginResponsePayload(user)) - } else { - Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`) - if (req.rateLimit.remaining <= 2) { - Logger.error(`[Auth] Failed login attempt for user ${user.username} from ip ${ipAddress}. Attempts: ${req.rateLimit.current}`) - return res.status(401).send(`Invalid user or password (${req.rateLimit.remaining === 0 ? '1 attempt remaining' : `${req.rateLimit.remaining + 1} attempts remaining`})`) - } - return res.status(401).send('Invalid user or password') - } - } - - // Not in use now - lockUser(user) { - user.isLocked = true - return this.db.updateEntity('user', user).catch((error) => { - Logger.error('[Auth] Failed to lock user', user.username, error) - return false - }) - } - - comparePassword(password, user) { - if (user.type === 'root' && !password && !user.pash) return true - if (!password || !user.pash) return false - return bcrypt.compare(password, user.pash) - } - - async userChangePassword(req, res) { - var { password, newPassword } = req.body - newPassword = newPassword || '' - var matchingUser = this.users.find(u => u.id === req.user.id) - - // Only root can have an empty password - if (matchingUser.type !== 'root' && !newPassword) { - return res.json({ - error: 'Invalid new password - Only root can have an empty password' - }) - } - - var compare = await this.comparePassword(password, matchingUser) - if (!compare) { - return res.json({ - error: 'Invalid password' - }) - } - - var pw = '' - if (newPassword) { - pw = await this.hashPass(newPassword) - if (!pw) { - return res.json({ - error: 'Hash failed' - }) - } - } - - matchingUser.pash = pw - var success = await this.db.updateEntity('user', matchingUser) - if (success) { - res.json({ - success: true - }) - } else { - res.json({ - error: 'Unknown error' - }) - } - } } + module.exports = Auth \ No newline at end of file diff --git a/server/Server.js b/server/Server.js index a5a0b8b5..17cd6256 100644 --- a/server/Server.js +++ b/server/Server.js @@ -37,6 +37,11 @@ const CronManager = require('./managers/CronManager') const TaskManager = require('./managers/TaskManager') const EBookManager = require('./managers/EBookManager') +//Import the main Passport and Express-Session library +const passport = require('passport') +const expressSession = require('express-session') + + class Server { constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) { this.Port = PORT @@ -48,7 +53,7 @@ class Server { global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH)) global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH)) global.RouterBasePath = ROUTER_BASE_PATH - global.XAccel = process.env.USE_X_ACCEL + global.XAccel = process.env.USE_X_ACCELAuth if (!fs.pathExistsSync(global.ConfigPath)) { fs.mkdirSync(global.ConfigPath) @@ -92,7 +97,7 @@ class Server { } authMiddleware(req, res, next) { - this.auth.authMiddleware(req, res, next) + this.auth.isAuthenticated(req, res, next) } async init() { @@ -141,13 +146,33 @@ class Server { await this.init() const app = express() + + // enable express-session + app.use(expressSession({ + secret: global.ServerSettings.tokenSecret, + resave: false, + saveUninitialized: false, + cookie: { + // also send the cookie if were hare not on https + secure: false + }, + })) + // init passport.js + app.use(passport.initialize()) + // register passport in express-session + app.use(passport.session()) + // config passport.js + this.auth.initPassportJs() + // use auth on all routes - not used now + // app.use(passport.authenticate('session')); + const router = express.Router() app.use(global.RouterBasePath, router) app.disable('x-powered-by') this.server = http.createServer(app) - router.use(this.auth.cors) + // router.use(this.auth.cors) router.use(fileUpload()) router.use(express.urlencoded({ extended: true, limit: "5mb" })); router.use(express.json({ limit: "5mb" })) @@ -166,6 +191,9 @@ class Server { router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router) router.use('/s', this.authMiddleware.bind(this), this.staticRouter.router) + // Auth routes + this.auth.initAuthRoutes(router) + // EBook static file routes router.get('/ebook/:library/:folder/*', (req, res) => { const library = this.db.libraries.find(lib => lib.id === req.params.library) @@ -213,8 +241,8 @@ class Server { ] dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html')))) - router.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res)) - router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this)) + // router.post('/login', passport.authenticate('local', this.auth.login), this.auth.loginResult.bind(this)) + // router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this)) router.post('/init', (req, res) => { if (this.db.hasRootUser) { Logger.error(`[Server] attempt to init server when server already has a root user`) diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index af0ce0e1..6e39f6f4 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -43,7 +43,7 @@ class UserController { account.id = getId('usr') account.pash = await this.auth.hashPass(account.password) delete account.password - account.token = await this.auth.generateAccessToken({ userId: account.id, username }) + account.token = await this.auth.generateAccessToken(account) account.createdAt = Date.now() var newUser = new User(account) var success = await this.db.insertEntity('user', newUser) @@ -85,7 +85,7 @@ class UserController { var hasUpdated = user.update(account) if (hasUpdated) { if (shouldUpdateToken) { - user.token = await this.auth.generateAccessToken({ userId: user.id, username: user.username }) + user.token = await this.auth.generateAccessToken(user) Logger.info(`[UserController] User ${user.username} was generated a new api token`) } await this.db.updateEntity('user', user) diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 38b75370..9bdc7796 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -57,6 +57,16 @@ class ServerSettings { this.version = null + // Auth settings + // Active auth methodes + this.authActiveAuthMethods = ['local'] + + // google-oauth20 settings + this.authGoogleOauth20ClientID = '' + this.authGoogleOauth20ClientSecret = '' + this.authGoogleOauth20CallbackURL = '' + + if (settings) { this.construct(settings) } @@ -100,6 +110,30 @@ class ServerSettings { this.logLevel = settings.logLevel || Logger.logLevel this.version = settings.version || null + this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local'] + this.authGoogleOauth20ClientID = settings.authGoogleOauth20ClientID || '' + this.authGoogleOauth20ClientSecret = settings.authGoogleOauth20ClientSecret || '' + this.authGoogleOauth20CallbackURL = settings.authGoogleOauth20CallbackURL || '' + + if (!Array.isArray(this.authActiveAuthMethods)) { + this.authActiveAuthMethods = ['local'] + } + + // remove uninitialized methods + // GoogleOauth20 + if (this.authActiveAuthMethods.includes('google-oauth20') && ( + this.authGoogleOauth20ClientID === '' || + this.authGoogleOauth20ClientSecret === '' || + this.authGoogleOauth20CallbackURL === '' + )) { + this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('google-oauth20', 0), 1); + } + + // fallback to local + if (!Array.isArray(this.authActiveAuthMethods) || this.authActiveAuthMethods.length == 0) { + this.authActiveAuthMethods = ['local'] + } + // Migrations if (settings.storeCoverWithBook != undefined) { // storeCoverWithBook was renamed to storeCoverWithItem in 2.0.0 this.storeCoverWithItem = !!settings.storeCoverWithBook @@ -148,13 +182,19 @@ class ServerSettings { dateFormat: this.dateFormat, language: this.language, logLevel: this.logLevel, - version: this.version + version: this.version, + authActiveAuthMethods: this.authActiveAuthMethods, + authGoogleOauth20ClientID: this.authGoogleOauth20ClientID, // Do not return to client + authGoogleOauth20ClientSecret: this.authGoogleOauth20ClientSecret, // Do not return to client + authGoogleOauth20CallbackURL: this.authGoogleOauth20CallbackURL } } toJSONForBrowser() { const json = this.toJSON() delete json.tokenSecret + delete json.authGoogleOauth20ClientID + delete json.authGoogleOauth20ClientSecret return json } From 08676a675ae2e5cc945a008b60f00bee52f249fc Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Fri, 24 Mar 2023 18:31:58 +0100 Subject: [PATCH 02/39] Fix: small problem with this context in Auth.js --- server/Auth.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 4cb40da6..16889163 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -48,22 +48,22 @@ class Auth { process.nextTick(function () { // only store username and id to session // TODO: do we want to store more info in the session? - return cb(null, { + return cb(null, JSON.stringify({ "username": user.username, "id": user.id, - }); + })); }); }); // define how to deseralize a user (use the username to get it from the database) - passport.deserializeUser(function (user, cb) { - process.nextTick(function () { - parsedUserInfo = JSON.parse(user) + passport.deserializeUser((function (user, cb) { + process.nextTick((function () { + const parsedUserInfo = JSON.parse(user) // TODO: do the matching on username or better on id? var dbUser = this.db.users.find(u => u.username.toLowerCase() === parsedUserInfo.username.toLowerCase()) return cb(null, new User(dbUser)); - }); - }); + }).bind(this)); + }).bind(this)); } /** From 62b0940766c11df1e705756dc454a06b9b5117a4 Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Fri, 14 Apr 2023 20:26:29 +0200 Subject: [PATCH 03/39] Added passport-openidconnect implementation --- package-lock.json | 26 +++++++++++ package.json | 1 + server/Auth.js | 54 +++++++++++++++++++++-- server/objects/settings/ServerSettings.js | 41 ++++++++++++++++- 4 files changed, 118 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 82657aba..c32ea13b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", + "passport-openidconnect": "^0.1.1", "socket.io": "^4.5.4", "xml2js": "^0.4.23" }, @@ -1138,6 +1139,22 @@ "url": "https://github.com/sponsors/jaredhanson" } }, + "node_modules/passport-openidconnect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.1.tgz", + "integrity": "sha512-r0QJiWEzwCg2MeCIXVP5G6YxVRqnEsZ2HpgKRthZ9AiQHJrgGUytXpsdcGF9BRwd3yMrEesb/uG/Yxb86rrY0g==", + "dependencies": { + "oauth": "0.9.x", + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -2417,6 +2434,15 @@ "utils-merge": "1.x.x" } }, + "passport-openidconnect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.1.tgz", + "integrity": "sha512-r0QJiWEzwCg2MeCIXVP5G6YxVRqnEsZ2HpgKRthZ9AiQHJrgGUytXpsdcGF9BRwd3yMrEesb/uG/Yxb86rrY0g==", + "requires": { + "oauth": "0.9.x", + "passport-strategy": "1.x.x" + } + }, "passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", diff --git a/package.json b/package.json index 3190539a..95429a08 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", + "passport-openidconnect": "^0.1.1", "socket.io": "^4.5.4", "xml2js": "^0.4.23" }, diff --git a/server/Auth.js b/server/Auth.js index 16889163..40bb64ac 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -5,6 +5,7 @@ const LocalStrategy = require('passport-local') const JwtStrategy = require('passport-jwt').Strategy; const ExtractJwt = require('passport-jwt').ExtractJwt; const GoogleStrategy = require('passport-google-oauth20').Strategy; +var OpenIDConnectStrategy = require('passport-openidconnect'); const User = require('./objects/user/User.js') /** @@ -24,17 +25,52 @@ class Auth { if (global.ServerSettings.authActiveAuthMethods.includes("local")) { passport.use(new LocalStrategy(this.localAuthCheckUserPw.bind(this))) } + // Check if we should load the google-oauth20 strategy if (global.ServerSettings.authActiveAuthMethods.includes("google-oauth20")) { passport.use(new GoogleStrategy({ clientID: global.ServerSettings.authGoogleOauth20ClientID, clientSecret: global.ServerSettings.authGoogleOauth20ClientSecret, callbackURL: global.ServerSettings.authGoogleOauth20CallbackURL - }, function (accessToken, refreshToken, profile, done) { + }, (function (accessToken, refreshToken, profile, done) { // TODO: what to use as username // TODO: do we want to create the users which does not exist? - return done(null, { username: profile.emails[0].value }) - })) + var user = this.db.users.find(u => u.username.toLowerCase() === profile.emails[0].value.toLowerCase()) + + if (!user || !user.isActive) { + done(null, null) + return + } + + return done(null, user) + }).bind(this))) + } + + // Check if we should load the openid strategy + if (global.ServerSettings.authActiveAuthMethods.includes("openid")) { + passport.use(new OpenIDConnectStrategy({ + issuer: global.ServerSettings.authOpenIDIssuerURL, + authorizationURL: global.ServerSettings.authOpenIDAuthorizationURL, + tokenURL: global.ServerSettings.authOpenIDTokenURL, + userInfoURL: global.ServerSettings.authOpenIDUserInfoURL, + clientID: global.ServerSettings.authOpenIDClientID, + clientSecret: global.ServerSettings.authOpenIDClientSecret, + callbackURL: global.ServerSettings.authOpenIDCallbackURL, + scope: ["openid", "email", "profile"], + skipUserProfile: false + }, + (function (issuer, profile, done) { + // TODO: what to use as username + // TODO: do we want to create the users which does not exist? + var user = this.db.users.find(u => u.username.toLowerCase() === profile.emails[0].value.toLowerCase()) + + if (!user || !user.isActive) { + done(null, null) + return + } + + return done(null, user) + }).bind(this))) } // Load the JwtStrategy (always) -> for bearer token auth @@ -99,6 +135,18 @@ class Auth { }).bind(this) ) + // openid strategy login route (this redirects to the configured openid login provider) + router.get('/auth/openid', passport.authenticate('openidconnect')); + + // openid strategy callback route (this receives the token from the configured openid login provider) + router.get('/auth/openid/callback', + passport.authenticate('openidconnect', { failureRedirect: '/login' }), + (function (req, res) { + // return the user login response json if the login was successfull + res.json(this.getUserLoginResponsePayload(req.user.username)) + }).bind(this) + ) + // Logout route router.get('/logout', function (req, res) { // TODO: invalidate possible JWTs diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 53e00e9b..602d8ac1 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -67,6 +67,14 @@ class ServerSettings { this.authGoogleOauth20ClientSecret = '' this.authGoogleOauth20CallbackURL = '' + // generic-oauth20 settings + this.authOpenIDIssuerURL = '' + this.authOpenIDAuthorizationURL = '' + this.authOpenIDTokenURL = '' + this.authOpenIDUserInfoURL = '' + this.authOpenIDClientID = '' + this.authOpenIDClientSecret = '' + this.authOpenIDCallbackURL = '' if (settings) { this.construct(settings) @@ -117,6 +125,14 @@ class ServerSettings { this.authGoogleOauth20ClientSecret = settings.authGoogleOauth20ClientSecret || '' this.authGoogleOauth20CallbackURL = settings.authGoogleOauth20CallbackURL || '' + this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || '' + this.authOpenIDAuthorizationURL = settings.authOpenIDAuthorizationURL || '' + this.authOpenIDTokenURL = settings.authOpenIDTokenURL || '' + this.authOpenIDUserInfoURL = settings.authOpenIDUserInfoURL || '' + this.authOpenIDClientID = settings.authOpenIDClientID || '' + this.authOpenIDClientSecret = settings.authOpenIDClientSecret || '' + this.authOpenIDCallbackURL = settings.authOpenIDCallbackURL || '' + if (!Array.isArray(this.authActiveAuthMethods)) { this.authActiveAuthMethods = ['local'] } @@ -131,6 +147,20 @@ class ServerSettings { this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('google-oauth20', 0), 1); } + // remove uninitialized methods + // OpenID + if (this.authActiveAuthMethods.includes('generic-oauth20') && ( + this.authOpenIDIssuerURL === '' || + this.authOpenIDAuthorizationURL === '' || + this.authOpenIDTokenURL === '' || + this.authOpenIDUserInfoURL === '' || + this.authOpenIDClientID === '' || + this.authOpenIDClientSecret === '' || + this.authOpenIDCallbackURL === '' + )) { + this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('generic-oauth20', 0), 1); + } + // fallback to local if (!Array.isArray(this.authActiveAuthMethods) || this.authActiveAuthMethods.length == 0) { this.authActiveAuthMethods = ['local'] @@ -189,7 +219,14 @@ class ServerSettings { authActiveAuthMethods: this.authActiveAuthMethods, authGoogleOauth20ClientID: this.authGoogleOauth20ClientID, // Do not return to client authGoogleOauth20ClientSecret: this.authGoogleOauth20ClientSecret, // Do not return to client - authGoogleOauth20CallbackURL: this.authGoogleOauth20CallbackURL + authGoogleOauth20CallbackURL: this.authGoogleOauth20CallbackURL, + authOpenIDIssuerURL: this.authOpenIDIssuerURL, + authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL, + authOpenIDTokenURL: this.authOpenIDTokenURL, + authOpenIDUserInfoURL: this.authOpenIDUserInfoURL, + authOpenIDClientID: this.authOpenIDClientID, // Do not return to client + authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client + authOpenIDCallbackURL: this.authOpenIDCallbackURL } } @@ -198,6 +235,8 @@ class ServerSettings { delete json.tokenSecret delete json.authGoogleOauth20ClientID delete json.authGoogleOauth20ClientSecret + delete json.authOpenIDClientID + delete json.authOpenIDClientSecret return json } From 7010a13648860288b2be43a230f498499e13f0f4 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 16 Apr 2023 10:08:13 -0500 Subject: [PATCH 04/39] Fixes for passport local and allow empty password --- client/pages/login.vue | 9 +- package-lock.json | 20 ---- package.json | 1 - server/Auth.js | 76 +++++++------- server/Server.js | 5 +- server/SocketAuthority.js | 2 +- server/libs/passportLocal/LICENSE | 20 ++++ server/libs/passportLocal/index.js | 20 ++++ server/libs/passportLocal/strategy.js | 119 ++++++++++++++++++++++ server/objects/settings/ServerSettings.js | 9 +- 10 files changed, 206 insertions(+), 75 deletions(-) create mode 100644 server/libs/passportLocal/LICENSE create mode 100644 server/libs/passportLocal/index.js create mode 100644 server/libs/passportLocal/strategy.js diff --git a/client/pages/login.vue b/client/pages/login.vue index a94045ae..8da68f5e 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -124,7 +124,7 @@ export default { location.reload() }, - setUser({ user, userDefaultLibraryId, serverSettings, Source, feeds }) { + setUser({ user, userDefaultLibraryId, serverSettings, Source }) { this.$store.commit('setServerSettings', serverSettings) this.$store.commit('setSource', Source) this.$setServerLanguageCode(serverSettings.language) @@ -143,17 +143,18 @@ export default { this.error = null this.processing = true - var payload = { + const payload = { username: this.username, password: this.password || '' } - var authRes = await this.$axios.$post('/login', payload).catch((error) => { + const authRes = await this.$axios.$post('/login', payload).catch((error) => { console.error('Failed', error.response) if (error.response) this.error = error.response.data else this.error = 'Unknown Error' return false }) - if (authRes && authRes.error) { + console.log('Auth res', authRes) + if (authRes?.error) { this.error = authRes.error } else if (authRes) { this.setUser(authRes) diff --git a/package-lock.json b/package-lock.json index 0bfa07d2..e87a4b5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", - "passport-local": "^1.0.0", "passport-openidconnect": "^0.1.1", "socket.io": "^4.5.4", "xml2js": "^0.4.23" @@ -1109,17 +1108,6 @@ "passport-strategy": "^1.0.0" } }, - "node_modules/passport-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", - "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", - "dependencies": { - "passport-strategy": "1.x.x" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/passport-oauth2": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", @@ -2414,14 +2402,6 @@ "passport-strategy": "^1.0.0" } }, - "passport-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", - "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", - "requires": { - "passport-strategy": "1.x.x" - } - }, "passport-oauth2": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", diff --git a/package.json b/package.json index b5ee0bd1..43ab49e2 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", - "passport-local": "^1.0.0", "passport-openidconnect": "^0.1.1", "socket.io": "^4.5.4", "xml2js": "^0.4.23" diff --git a/server/Auth.js b/server/Auth.js index 40bb64ac..9e1b2a12 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -1,12 +1,11 @@ const passport = require('passport') const bcrypt = require('./libs/bcryptjs') const jwt = require('./libs/jsonwebtoken') -const LocalStrategy = require('passport-local') -const JwtStrategy = require('passport-jwt').Strategy; -const ExtractJwt = require('passport-jwt').ExtractJwt; -const GoogleStrategy = require('passport-google-oauth20').Strategy; -var OpenIDConnectStrategy = require('passport-openidconnect'); -const User = require('./objects/user/User.js') +const LocalStrategy = require('./libs/passportLocal') +const JwtStrategy = require('passport-jwt').Strategy +const ExtractJwt = require('passport-jwt').ExtractJwt +const GoogleStrategy = require('passport-google-oauth20').Strategy +const OpenIDConnectStrategy = require('passport-openidconnect') /** * @class Class for handling all the authentication related functionality. @@ -35,7 +34,7 @@ class Auth { }, (function (accessToken, refreshToken, profile, done) { // TODO: what to use as username // TODO: do we want to create the users which does not exist? - var user = this.db.users.find(u => u.username.toLowerCase() === profile.emails[0].value.toLowerCase()) + const user = this.db.users.find(u => u.username.toLowerCase() === profile.emails[0].value.toLowerCase()) if (!user || !user.isActive) { done(null, null) @@ -87,19 +86,19 @@ class Auth { return cb(null, JSON.stringify({ "username": user.username, "id": user.id, - })); - }); - }); + })) + }) + }) // define how to deseralize a user (use the username to get it from the database) passport.deserializeUser((function (user, cb) { process.nextTick((function () { const parsedUserInfo = JSON.parse(user) // TODO: do the matching on username or better on id? - var dbUser = this.db.users.find(u => u.username.toLowerCase() === parsedUserInfo.username.toLowerCase()) - return cb(null, new User(dbUser)); - }).bind(this)); - }).bind(this)); + const dbUser = this.db.users.find(u => u.username.toLowerCase() === parsedUserInfo.username.toLowerCase()) + return cb(null, dbUser) + }).bind(this)) + }).bind(this)) } /** @@ -107,19 +106,11 @@ class Auth { * @param {express.Router} router */ initAuthRoutes(router) { - // just a route saying "you need to login" where we redirect e.g. after logout - // TODO: replace with a 401? - router.get('/login', function (req, res) { - res.send('please login') - }) - // Local strategy login route (takes username and password) - router.post('/login', passport.authenticate('local', { - failureRedirect: '/login' - }), + router.post('/login', passport.authenticate('local'), (function (req, res) { // return the user login response json if the login was successfull - res.json(this.getUserLoginResponsePayload(req.user.username)) + res.json(this.getUserLoginResponsePayload(req.user)) }).bind(this) ) @@ -128,30 +119,35 @@ class Auth { // google-oauth20 strategy callback route (this receives the token from google) router.get('/auth/google/callback', - passport.authenticate('google', { failureRedirect: '/login' }), + passport.authenticate('google'), (function (req, res) { // return the user login response json if the login was successfull - res.json(this.getUserLoginResponsePayload(req.user.username)) + res.json(this.getUserLoginResponsePayload(req.user)) }).bind(this) ) // openid strategy login route (this redirects to the configured openid login provider) - router.get('/auth/openid', passport.authenticate('openidconnect')); + router.get('/auth/openid', passport.authenticate('openidconnect')) // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', - passport.authenticate('openidconnect', { failureRedirect: '/login' }), + passport.authenticate('openidconnect'), (function (req, res) { // return the user login response json if the login was successfull - res.json(this.getUserLoginResponsePayload(req.user.username)) + res.json(this.getUserLoginResponsePayload(req.user)) }).bind(this) ) // Logout route - router.get('/logout', function (req, res) { + router.post('/logout', (req, res) => { // TODO: invalidate possible JWTs - req.logout() - res.redirect('/login') + req.logout((err) => { + if (err) { + res.sendStatus(500) + } else { + res.sendStatus(200) + } + }) }) } @@ -177,7 +173,7 @@ class Auth { * @returns the token. */ generateAccessToken(user) { - return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret); + return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret) } /** @@ -206,7 +202,7 @@ class Auth { * @param {function} done */ jwtAuthCheck(jwt_payload, done) { - var user = this.db.users.find(u => u.username.toLowerCase() === jwt_payload.username.toLowerCase()) + const user = this.db.users.find(u => u.username.toLowerCase() === jwt_payload.username.toLowerCase()) if (!user || !user.isActive) { done(null, null) @@ -217,13 +213,13 @@ class Auth { } /** - * Checks if a username and passpword touple is valid and the user active. + * Checks if a username and password tuple is valid and the user active. * @param {string} username * @param {string} password * @param {function} done */ - localAuthCheckUserPw(username, password, done) { - var user = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase()) + async localAuthCheckUserPw(username, password, done) { + const user = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase()) if (!user || !user.isActive) { done(null, null) @@ -241,7 +237,7 @@ class Auth { } // Check password match - var compare = bcrypt.compareSync(password, user.pash) + const compare = await bcrypt.compare(password, user.pash) if (compare) { done(null, user) return @@ -272,9 +268,7 @@ class Auth { * @param {string} username * @returns {string} jsonPayload */ - getUserLoginResponsePayload(username) { - var user = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase()) - user = new User(user) + getUserLoginResponsePayload(user) { return { user: user.toJSONForBrowser(), userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries), diff --git a/server/Server.js b/server/Server.js index d2865b88..96896a73 100644 --- a/server/Server.js +++ b/server/Server.js @@ -161,7 +161,7 @@ class Server { // config passport.js this.auth.initPassportJs() // use auth on all routes - not used now - // app.use(passport.authenticate('session')); + // app.use(passport.authenticate('session')) const router = express.Router() app.use(global.RouterBasePath, router) @@ -169,9 +169,8 @@ class Server { this.server = http.createServer(app) - // router.use(this.auth.cors) router.use(fileUpload()) - router.use(express.urlencoded({ extended: true, limit: "5mb" })); + router.use(express.urlencoded({ extended: true, limit: "5mb" })) router.use(express.json({ limit: "5mb" })) // Static path to generated nuxt diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index bf6ea6f7..a12bca29 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -140,7 +140,7 @@ class SocketAuthority { // When setting up a socket connection the user needs to be associated with a socket id // for this the client will send a 'auth' event that includes the users API token async authenticateSocket(socket, token) { - const user = await this.Server.auth.authenticateUser(token) + const user = await this.Server.db.users.find(u => u.token === token) if (!user) { Logger.error('Cannot validate socket - invalid token') return socket.emit('invalid_token') diff --git a/server/libs/passportLocal/LICENSE b/server/libs/passportLocal/LICENSE new file mode 100644 index 00000000..d8ebfcf1 --- /dev/null +++ b/server/libs/passportLocal/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2011-2014 Jared Hanson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/server/libs/passportLocal/index.js b/server/libs/passportLocal/index.js new file mode 100644 index 00000000..365d4f65 --- /dev/null +++ b/server/libs/passportLocal/index.js @@ -0,0 +1,20 @@ +// +// modified for audiobookshelf +// Source: https://github.com/jaredhanson/passport-local +// + +/** + * Module dependencies. + */ +var Strategy = require('./strategy'); + + +/** + * Expose `Strategy` directly from package. + */ +exports = module.exports = Strategy; + +/** + * Export constructors. + */ +exports.Strategy = Strategy; diff --git a/server/libs/passportLocal/strategy.js b/server/libs/passportLocal/strategy.js new file mode 100644 index 00000000..67110204 --- /dev/null +++ b/server/libs/passportLocal/strategy.js @@ -0,0 +1,119 @@ +/** + * Module dependencies. + */ +const passport = require('passport-strategy') +const util = require('util') + + +function lookup(obj, field) { + if (!obj) { return null; } + var chain = field.split(']').join('').split('['); + for (var i = 0, len = chain.length; i < len; i++) { + var prop = obj[chain[i]]; + if (typeof (prop) === 'undefined') { return null; } + if (typeof (prop) !== 'object') { return prop; } + obj = prop; + } + return null; +} + +/** + * `Strategy` constructor. + * + * The local authentication strategy authenticates requests based on the + * credentials submitted through an HTML-based login form. + * + * Applications must supply a `verify` callback which accepts `username` and + * `password` credentials, and then calls the `done` callback supplying a + * `user`, which should be set to `false` if the credentials are not valid. + * If an exception occured, `err` should be set. + * + * Optionally, `options` can be used to change the fields in which the + * credentials are found. + * + * Options: + * - `usernameField` field name where the username is found, defaults to _username_ + * - `passwordField` field name where the password is found, defaults to _password_ + * - `passReqToCallback` when `true`, `req` is the first argument to the verify callback (default: `false`) + * + * Examples: + * + * passport.use(new LocalStrategy( + * function(username, password, done) { + * User.findOne({ username: username, password: password }, function (err, user) { + * done(err, user); + * }); + * } + * )); + * + * @param {Object} options + * @param {Function} verify + * @api public + */ +function Strategy(options, verify) { + if (typeof options == 'function') { + verify = options; + options = {}; + } + if (!verify) { throw new TypeError('LocalStrategy requires a verify callback'); } + + this._usernameField = options.usernameField || 'username'; + this._passwordField = options.passwordField || 'password'; + + passport.Strategy.call(this); + this.name = 'local'; + this._verify = verify; + this._passReqToCallback = options.passReqToCallback; +} + +/** + * Inherit from `passport.Strategy`. + */ +util.inherits(Strategy, passport.Strategy); + +/** + * Authenticate request based on the contents of a form submission. + * + * @param {Object} req + * @api protected + */ +Strategy.prototype.authenticate = function (req, options) { + options = options || {}; + var username = lookup(req.body, this._usernameField) + if (username === null) { + lookup(req.query, this._usernameField); + } + + var password = lookup(req.body, this._passwordField) + if (password === null) { + password = lookup(req.query, this._passwordField); + } + + if (username === null || password === null) { + return this.fail({ message: options.badRequestMessage || 'Missing credentials' }, 400); + } + + var self = this; + + function verified(err, user, info) { + if (err) { return self.error(err); } + if (!user) { return self.fail(info); } + self.success(user, info); + } + + try { + if (self._passReqToCallback) { + this._verify(req, username, password, verified); + } else { + this._verify(username, password, verified); + } + } catch (ex) { + return self.error(ex); + } +}; + + +/** + * Expose `Strategy`. + */ +module.exports = Strategy; diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 602d8ac1..2a51e3de 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -1,5 +1,4 @@ const { BookshelfView } = require('../../utils/constants') -const { isNullOrNaN } = require('../../utils') const Logger = require('../../Logger') class ServerSettings { @@ -144,7 +143,7 @@ class ServerSettings { this.authGoogleOauth20ClientSecret === '' || this.authGoogleOauth20CallbackURL === '' )) { - this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('google-oauth20', 0), 1); + this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('google-oauth20', 0), 1) } // remove uninitialized methods @@ -158,7 +157,7 @@ class ServerSettings { this.authOpenIDClientSecret === '' || this.authOpenIDCallbackURL === '' )) { - this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('generic-oauth20', 0), 1); + this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('generic-oauth20', 0), 1) } // fallback to local @@ -241,10 +240,10 @@ class ServerSettings { } update(payload) { - var hasUpdates = false + let hasUpdates = false for (const key in payload) { if (key === 'sortingPrefixes' && payload[key] && payload[key].length) { - var prefixesCleaned = payload[key].filter(prefix => !!prefix).map(prefix => prefix.toLowerCase()) + const prefixesCleaned = payload[key].filter(prefix => !!prefix).map(prefix => prefix.toLowerCase()) if (prefixesCleaned.join(',') !== this[key].join(',')) { this[key] = [...prefixesCleaned] hasUpdates = true From 4359ca28dfb034d27c1847382554102b1305632b Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 29 Apr 2023 16:05:05 -0500 Subject: [PATCH 05/39] Fix XAccel issue --- server/Server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/Server.js b/server/Server.js index 96896a73..1559a88e 100644 --- a/server/Server.js +++ b/server/Server.js @@ -52,7 +52,7 @@ class Server { global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH)) global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH)) global.RouterBasePath = ROUTER_BASE_PATH - global.XAccel = process.env.USE_X_ACCELAuth + global.XAccel = process.env.USE_X_ACCEL if (!fs.pathExistsSync(global.ConfigPath)) { fs.mkdirSync(global.ConfigPath) From 405c954b65fa1cf76b4cd9374ec0cb62b7ff61f5 Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Wed, 13 Sep 2023 16:35:39 +0000 Subject: [PATCH 06/39] Updated + first rough implementation --- client/pages/login.vue | 21 ++++++++++----- server/Auth.js | 54 +++++++++++++++++++++++++++------------ server/Server.js | 6 ++++- server/SocketAuthority.js | 29 ++++++++++++++++++++- server/models/User.js | 19 ++++++++++++++ 5 files changed, 104 insertions(+), 25 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index 52feedb6..27bdc63b 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -37,6 +37,15 @@ {{ processing ? 'Checking...' : $strings.ButtonSubmit }} +
+ @@ -132,11 +141,7 @@ export default { location.reload() }, -<<<<<<< HEAD - setUser({ user, userDefaultLibraryId, serverSettings, Source }) { -======= setUser({ user, userDefaultLibraryId, serverSettings, Source, ereaderDevices }) { ->>>>>>> origin/master this.$store.commit('setServerSettings', serverSettings) this.$store.commit('setSource', Source) this.$store.commit('libraries/setEReaderDevices', ereaderDevices) @@ -166,10 +171,7 @@ export default { else this.error = 'Unknown Error' return false }) -<<<<<<< HEAD console.log('Auth res', authRes) -======= ->>>>>>> origin/master if (authRes?.error) { this.error = authRes.error } else if (authRes) { @@ -222,6 +224,11 @@ export default { } }, async mounted() { + console.log(new URLSearchParams(window.location.search).get('setToken')) + if (new URLSearchParams(window.location.search).get('setToken')) { + localStorage.setItem('token', new URLSearchParams(window.location.search).get('setToken')) + console.log('hereasd') + } if (localStorage.getItem('token')) { var userfound = await this.checkAuth() if (userfound) return // if valid user no need to check status diff --git a/server/Auth.js b/server/Auth.js index 0885c88a..ceeddb36 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -6,14 +6,14 @@ const JwtStrategy = require('passport-jwt').Strategy const ExtractJwt = require('passport-jwt').ExtractJwt const GoogleStrategy = require('passport-google-oauth20').Strategy const OpenIDConnectStrategy = require('passport-openidconnect') +const Database = require('./Database') /** * @class Class for handling all the authentication related functionality. */ class Auth { - constructor(db) { - this.db = db + constructor() { } /** @@ -31,10 +31,10 @@ class Auth { clientID: global.ServerSettings.authGoogleOauth20ClientID, clientSecret: global.ServerSettings.authGoogleOauth20ClientSecret, callbackURL: global.ServerSettings.authGoogleOauth20CallbackURL - }, (function (accessToken, refreshToken, profile, done) { + }, (async function (accessToken, refreshToken, profile, done) { // TODO: what to use as username // TODO: do we want to create the users which does not exist? - const user = this.db.users.find(u => u.username.toLowerCase() === profile.emails[0].value.toLowerCase()) + const user = await Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) if (!user || !user.isActive) { done(null, null) @@ -61,7 +61,7 @@ class Auth { (function (issuer, profile, done) { // TODO: what to use as username // TODO: do we want to create the users which does not exist? - var user = this.db.users.find(u => u.username.toLowerCase() === profile.emails[0].value.toLowerCase()) + var user = Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) if (!user || !user.isActive) { done(null, null) @@ -86,16 +86,17 @@ class Auth { return cb(null, JSON.stringify({ "username": user.username, "id": user.id, + "email": user.email, })) }) }) // define how to deseralize a user (use the username to get it from the database) passport.deserializeUser((function (user, cb) { - process.nextTick((function () { + process.nextTick((async function () { const parsedUserInfo = JSON.parse(user) // TODO: do the matching on username or better on id? - const dbUser = this.db.users.find(u => u.username.toLowerCase() === parsedUserInfo.username.toLowerCase()) + const dbUser = await Database.userModel.getUserByUsername(parsedUserInfo.username.toLowerCase()) return cb(null, dbUser) }).bind(this)) }).bind(this)) @@ -108,9 +109,9 @@ class Auth { initAuthRoutes(router) { // Local strategy login route (takes username and password) router.post('/login', passport.authenticate('local'), - (function (req, res) { + (async function (req, res) { // return the user login response json if the login was successfull - res.json(this.getUserLoginResponsePayload(req.user)) + res.json(await this.getUserLoginResponsePayload(req.user)) }).bind(this) ) @@ -120,9 +121,12 @@ class Auth { // google-oauth20 strategy callback route (this receives the token from google) router.get('/auth/google/callback', passport.authenticate('google'), - (function (req, res) { + (async function (req, res) { // return the user login response json if the login was successfull - res.json(this.getUserLoginResponsePayload(req.user)) + var data_json = await this.getUserLoginResponsePayload(req.user) + // res.json(data_json) + // TODO: figure out how to redirect back to the app page + res.redirect(301, `http://localhost:3000/login?setToken=${data_json.user.token}`) }).bind(this) ) @@ -132,9 +136,12 @@ class Auth { // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', passport.authenticate('openidconnect'), - (function (req, res) { + (async function (req, res) { // return the user login response json if the login was successfull - res.json(this.getUserLoginResponsePayload(req.user)) + var data_json = await this.getUserLoginResponsePayload(req.user) + // res.json(data_json) + // TODO: figure out how to redirect back to the app page + res.redirect(301, `http://localhost:3000/login?setToken=${data_json.user.token}`) }).bind(this) ) @@ -176,6 +183,20 @@ class Auth { return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret) } + /** + * Function to generate a jwt token for a given user. + * @param {string} token + * @returns the tokens data. + */ + static validateAccessToken(token) { + try { + return jwt.verify(token, global.ServerSettings.tokenSecret) + } + catch (err) { + return null + } + } + /** * Generate a token for each user. */ @@ -203,7 +224,7 @@ class Auth { * @param {function} done */ jwtAuthCheck(jwt_payload, done) { - const user = this.db.users.find(u => u.username.toLowerCase() === jwt_payload.username.toLowerCase()) + const user = Database.userModel.getUserByUsername(jwt_payload.username.toLowerCase()) if (!user || !user.isActive) { done(null, null) @@ -220,7 +241,7 @@ class Auth { * @param {function} done */ async localAuthCheckUserPw(username, password, done) { - const user = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase()) + const user = Database.userModel.getUserByUsername(username.toLowerCase()) if (!user || !user.isActive) { done(null, null) @@ -269,7 +290,8 @@ class Auth { * @param {string} username * @returns {string} jsonPayload */ - getUserLoginResponsePayload(user) { + async getUserLoginResponsePayload(user) { + const libraryIds = await Database.libraryModel.getAllLibraryIds() return { user: user.toJSONForBrowser(), userDefaultLibraryId: user.getDefaultLibraryId(libraryIds), diff --git a/server/Server.js b/server/Server.js index 9156c021..89985768 100644 --- a/server/Server.js +++ b/server/Server.js @@ -161,7 +161,8 @@ class Server { this.server = http.createServer(app) - router.use(this.auth.cors) + + router.use(fileUpload({ defCharset: 'utf8', defParamCharset: 'utf8', @@ -195,6 +196,9 @@ class Server { this.rssFeedManager.getFeedItem(req, res) }) + // Auth routes + this.auth.initAuthRoutes(router) + // Client dynamic routes const dyanimicRoutes = [ '/item/:id', diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index 5c3c8715..81136ecf 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -1,6 +1,9 @@ const SocketIO = require('socket.io') const Logger = require('./Logger') const Database = require('./Database') +const Auth = require('./Auth') +const passport = require('passport') +const expressSession = require('express-session') class SocketAuthority { constructor() { @@ -81,6 +84,24 @@ class SocketAuthority { methods: ["GET", "POST"] } }) + + /* + const wrap = middleware => (socket, next) => middleware(socket.request, {}, next); + + io.use(wrap(expressSession({ + secret: global.ServerSettings.tokenSecret, + resave: false, + saveUninitialized: false, + cookie: { + // also send the cookie if were hare not on https + secure: false + }, + }))); + + io.use(wrap(passport.initialize())); + io.use(wrap(passport.session())); + */ + this.io.on('connection', (socket) => { this.clients[socket.id] = { id: socket.id, @@ -147,7 +168,13 @@ class SocketAuthority { // When setting up a socket connection the user needs to be associated with a socket id // for this the client will send a 'auth' event that includes the users API token async authenticateSocket(socket, token) { - const user = await this.Server.db.users.find(u => u.token === token) + // TODO + const token_data = Auth.validateAccessToken(token) + if (!token_data || !token_data.username) { + Logger.error('Cannot validate socket - invalid token') + return socket.emit('invalid_token') + } + const user = await Database.userModel.getUserByUsername(token_data.username) if (!user) { Logger.error('Cannot validate socket - invalid token') return socket.emit('invalid_token') diff --git a/server/models/User.js b/server/models/User.js index 6f457aa5..5e184fc7 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -194,6 +194,25 @@ class User extends Model { return this.getOldUser(user) } + /** + * Get user by email case insensitive + * @param {string} username + * @returns {Promise} returns null if not found + */ + static async getUserByEmail(email) { + if (!email) return null + const user = await this.findOne({ + where: { + email: { + [Op.like]: email + } + }, + include: this.sequelize.models.mediaProgress + }) + if (!user) return null + return this.getOldUser(user) + } + /** * Get user by id * @param {string} userId From af4c35069be11663deeed76fe26a73105032d87b Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Thu, 14 Sep 2023 18:49:19 +0100 Subject: [PATCH 07/39] Use a short-time cookie to remember where to callback to --- client/pages/login.vue | 7 +- package-lock.json | 2162 +++++++++++++++++++++++++++++++++++++++- package.json | 5 +- server/Auth.js | 49 +- server/Server.js | 3 + 5 files changed, 2212 insertions(+), 14 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index 27bdc63b..1b17ab50 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -39,10 +39,10 @@
@@ -69,7 +69,8 @@ export default { }, confirmPassword: '', ConfigPath: '', - MetadataPath: '' + MetadataPath: '', + currentUrl: location.toString() } }, watch: { diff --git a/package-lock.json b/package-lock.json index 397babb5..967439f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", + "cookie-parser": "^1.4.6", "express": "^4.17.1", "express-session": "^1.17.3", "graceful-fs": "^4.2.10", @@ -533,6 +534,26 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -2881,5 +2902,2144 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } + }, + "dependencies": { + "@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, + "@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "requires": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "dependencies": { + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "requires": { + "abbrev": "1" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "optional": true, + "requires": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "optional": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "optional": true, + "requires": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "optional": true + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "requires": { + "@types/node": "*" + } + }, + "@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "requires": { + "@types/ms": "*" + } + }, + "@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + }, + "@types/node": { + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" + }, + "@types/validator": { + "version": "13.11.1", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.1.tgz", + "integrity": "sha512-d/MUkJYdOeKycmm75Arql4M5+UuXmf4cHdHKsyw1GcvnNgL6s77UkgSgJ8TE/rI5PYsnwYq5jkcWBLuN/MpQ1A==" + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "optional": true, + "requires": { + "humanize-ms": "^1.2.1" + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "optional": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "requires": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "optional": true, + "requires": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "optional": true + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "requires": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + } + } + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==" + }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", + "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.1" + } + }, + "dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "engine.io": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz", + "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.2.3" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "engine.io-parser": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz", + "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==" + }, + "entities": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==" + }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true + }, + "err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "requires": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + } + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + } + }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "htmlparser2": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", + "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "entities": "^4.3.0" + } + }, + "http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "optional": true + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "optional": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "optional": true, + "requires": { + "ms": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "optional": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "optional": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true + }, + "inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "optional": true + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "optional": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "optional": true + }, + "jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "requires": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "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==", + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + } + } + }, + "make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "optional": true, + "requires": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "optional": true, + "requires": { + "encoding": "^0.1.12", + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" + }, + "moment-timezone": { + "version": "0.5.43", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz", + "integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==", + "requires": { + "moment": "^2.29.4" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" + }, + "node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "optional": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "dependencies": { + "are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + } + }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "requires": { + "abbrev": "1" + } + }, + "npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "optional": true, + "requires": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "optional": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "node-tone": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz", + "integrity": "sha512-wi7L0taDZMN6tM5l85TDKHsYzdhqJTtPNgvgpk2zHeZzPt6ZIUZ9vBLTJRRDpm0xzCvbsvFHjAaudeQjLHTE4w==" + }, + "nodemailer": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.4.tgz", + "integrity": "sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA==" + }, + "nodemon": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", + "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", + "dev": true, + "requires": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "requires": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + } + }, + "passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "requires": { + "passport-oauth2": "1.x.x" + } + }, + "passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "requires": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "passport-oauth2": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", + "integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==", + "requires": { + "base64url": "3.x.x", + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + } + }, + "passport-openidconnect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.1.tgz", + "integrity": "sha512-r0QJiWEzwCg2MeCIXVP5G6YxVRqnEsZ2HpgKRthZ9AiQHJrgGUytXpsdcGF9BRwd3yMrEesb/uG/Yxb86rrY0g==", + "requires": { + "oauth": "0.9.x", + "passport-strategy": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "optional": true + }, + "promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true + }, + "retry-as-promised": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", + "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==" + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "sequelize": { + "version": "6.32.1", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.32.1.tgz", + "integrity": "sha512-3Iv0jruv57Y0YvcxQW7BE56O7DC1BojcfIrqh6my+IQwde+9u/YnuYHzK+8kmZLhLvaziRT1eWu38nh9yVwn/g==", + "requires": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.4", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.0", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.1", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==" + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "requires": { + "semver": "~7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "optional": true + }, + "socket.io": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz", + "integrity": "sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.2.1", + "socket.io-adapter": "~2.4.0", + "socket.io-parser": "~4.2.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-adapter": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz", + "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==" + }, + "socket.io-parser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz", + "integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "optional": true, + "requires": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "optional": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "sqlite3": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz", + "integrity": "sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==", + "requires": { + "@mapbox/node-pre-gyp": "^1.0.0", + "node-addon-api": "^4.2.0", + "node-gyp": "8.x", + "tar": "^6.1.11" + } + }, + "ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "optional": true, + "requires": { + "minipass": "^3.1.1" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tar": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", + "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" + } + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==" + }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, + "uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "optional": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "optional": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "optional": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "requires": { + "@types/node": "*" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "requires": {} + }, + "xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 182c3033..8c765640 100644 --- a/package.json +++ b/package.json @@ -31,16 +31,17 @@ "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", + "cookie-parser": "^1.4.6", "express": "^4.17.1", "express-session": "^1.17.3", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", "node-tone": "^1.0.1", + "nodemailer": "^6.9.2", "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "passport-openidconnect": "^0.1.1", - "nodemailer": "^6.9.2", "sequelize": "^6.32.1", "socket.io": "^4.5.4", "sqlite3": "^5.1.6", @@ -49,4 +50,4 @@ "devDependencies": { "nodemon": "^2.0.20" } -} \ No newline at end of file +} diff --git a/server/Auth.js b/server/Auth.js index ceeddb36..a219129b 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -32,7 +32,6 @@ class Auth { clientSecret: global.ServerSettings.authGoogleOauth20ClientSecret, callbackURL: global.ServerSettings.authGoogleOauth20CallbackURL }, (async function (accessToken, refreshToken, profile, done) { - // TODO: what to use as username // TODO: do we want to create the users which does not exist? const user = await Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) @@ -59,7 +58,6 @@ class Auth { skipUserProfile: false }, (function (issuer, profile, done) { - // TODO: what to use as username // TODO: do we want to create the users which does not exist? var user = Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) @@ -116,7 +114,20 @@ class Auth { ) // google-oauth20 strategy login route (this redirects to the google login) - router.get('/auth/google', passport.authenticate('google', { scope: ['email'] })) + router.get('/auth/google', (req, res, next) => { + const auth_func = passport.authenticate('google', { scope: ['email'] }) + if (!req.query.callback || req.query.callback === "") { + res.status(400).send({ + message: 'No callback parameter' + }) + return + } + res.cookie('auth_cb', req.query.callback, { + maxAge: 120000 * 120, // Hack - this semms to be in UTC?? + httpOnly: true + }) + auth_func(req, res, next); + }) // google-oauth20 strategy callback route (this receives the token from google) router.get('/auth/google/callback', @@ -125,13 +136,31 @@ class Auth { // return the user login response json if the login was successfull var data_json = await this.getUserLoginResponsePayload(req.user) // res.json(data_json) - // TODO: figure out how to redirect back to the app page - res.redirect(301, `http://localhost:3000/login?setToken=${data_json.user.token}`) + // TODO: do we want to somehow limit the values for auth_cb? + if (req.cookies.auth_cb) { + res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`) + } + else { + res.status(400).send("No callback or already expired") + } }).bind(this) ) // openid strategy login route (this redirects to the configured openid login provider) - router.get('/auth/openid', passport.authenticate('openidconnect')) + router.get('/auth/openid', (req, res, next) => { + const auth_func = passport.authenticate('openidconnect') + if (!req.query.callback || req.query.callback === "") { + res.status(400).send({ + message: 'No callback parameter' + }) + return + } + res.cookie('auth_cb', req.query.callback, { + maxAge: 120000 * 120, // Hack - this semms to be in UTC?? + httpOnly: true + }) + auth_func(req, res, next); + }) // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', @@ -140,8 +169,12 @@ class Auth { // return the user login response json if the login was successfull var data_json = await this.getUserLoginResponsePayload(req.user) // res.json(data_json) - // TODO: figure out how to redirect back to the app page - res.redirect(301, `http://localhost:3000/login?setToken=${data_json.user.token}`) + if (req.cookies.auth_cb) { + res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`) + } + else { + res.status(400).send("No callback or already expired") + } }).bind(this) ) diff --git a/server/Server.js b/server/Server.js index 89985768..ad7fa16c 100644 --- a/server/Server.js +++ b/server/Server.js @@ -5,6 +5,7 @@ const http = require('http') const fs = require('./libs/fsExtra') const fileUpload = require('./libs/expressFileupload') const rateLimit = require('./libs/expressRateLimit') +const cookieParser = require("cookie-parser"); const { version } = require('../package.json') @@ -136,6 +137,8 @@ class Server { const app = express() + // parse cookies in requests + app.use(cookieParser()); // enable express-session app.use(expressSession({ secret: global.ServerSettings.tokenSecret, From 6aaf3f0f025f2c95caa77055cedd77c1bb995615 Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Sat, 16 Sep 2023 18:22:11 +0000 Subject: [PATCH 08/39] Fix bug with undefined property --- server/Auth.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index a219129b..dc21aa37 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -235,9 +235,9 @@ class Auth { */ async initTokenSecret() { if (process.env.TOKEN_SECRET) { // User can supply their own token secret - this.db.serverSettings.tokenSecret = process.env.TOKEN_SECRET + global.ServerSettings.tokenSecret = process.env.TOKEN_SECRET } else { - this.db.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64') + global.ServerSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64') } await Database.updateServerSettings() From 91d8451ab38d45c10709ed7ea52d461e00ecd753 Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Sat, 16 Sep 2023 18:22:23 +0000 Subject: [PATCH 09/39] Remove log messages --- client/pages/login.vue | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index 1b17ab50..beac44ff 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -172,7 +172,7 @@ export default { else this.error = 'Unknown Error' return false }) - console.log('Auth res', authRes) + if (authRes?.error) { this.error = authRes.error } else if (authRes) { @@ -225,10 +225,8 @@ export default { } }, async mounted() { - console.log(new URLSearchParams(window.location.search).get('setToken')) if (new URLSearchParams(window.location.search).get('setToken')) { localStorage.setItem('token', new URLSearchParams(window.location.search).get('setToken')) - console.log('hereasd') } if (localStorage.getItem('token')) { var userfound = await this.checkAuth() From 7af3033f8d46275f105e4701455caa4b710013a5 Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Sat, 16 Sep 2023 18:42:48 +0000 Subject: [PATCH 10/39] Fix: ci error - no token sercret --- server/Auth.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/Auth.js b/server/Auth.js index dc21aa37..c8376ff6 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -19,7 +19,7 @@ class Auth { /** * Inializes all passportjs stragegies and other passportjs ralated initialization. */ - initPassportJs() { + async initPassportJs() { // Check if we should load the local strategy if (global.ServerSettings.authActiveAuthMethods.includes("local")) { passport.use(new LocalStrategy(this.localAuthCheckUserPw.bind(this))) @@ -70,6 +70,10 @@ class Auth { }).bind(this))) } + if (!global.ServerSettings.tokenSecret) { + await this.initTokenSecret() + } + // Load the JwtStrategy (always) -> for bearer token auth passport.use(new JwtStrategy({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), From 763c0f4a3df1bd4dce5c037aec0ae3d61e84633d Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Sat, 16 Sep 2023 18:51:29 +0000 Subject: [PATCH 11/39] add missing await --- server/Server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/Server.js b/server/Server.js index f306a61d..7cc6fa76 100644 --- a/server/Server.js +++ b/server/Server.js @@ -154,7 +154,7 @@ class Server { // register passport in express-session app.use(passport.session()) // config passport.js - this.auth.initPassportJs() + await this.auth.initPassportJs() // use auth on all routes - not used now // app.use(passport.authenticate('session')) From 942aa93f574af4a6b5ac8724c548b1ac484ef6cb Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Sat, 16 Sep 2023 19:45:04 +0000 Subject: [PATCH 12/39] Fix: local login not possible --- server/Auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/Auth.js b/server/Auth.js index c8376ff6..0e62d7dd 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -278,7 +278,7 @@ class Auth { * @param {function} done */ async localAuthCheckUserPw(username, password, done) { - const user = Database.userModel.getUserByUsername(username.toLowerCase()) + const user = await Database.userModel.getUserByUsername(username.toLowerCase()) if (!user || !user.isActive) { done(null, null) From 0a6cd8909068646db8c09d2b737b9e5f4eda240e Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Sun, 17 Sep 2023 18:42:42 +0100 Subject: [PATCH 13/39] Allow rest mode login (?isRest=true) --- server/Auth.js | 108 ++++++++++++++++++++++++++++--------------------- 1 file changed, 63 insertions(+), 45 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 0e62d7dd..17fea247 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -104,6 +104,63 @@ class Auth { }).bind(this)) } + /** + * Stores the client's choise how the login callback should happen in temp cookies. + * @param {*} req Request object. + * @param {*} res Response object. + */ + paramsToCookies(req, res) { + if (req.query.isRest && (req.query.isRest.toLowerCase() == "true" || req.query.isRest.toLowerCase() == "false")) { + res.cookie('is_rest', req.query.isRest.toLowerCase(), { + maxAge: 120000 * 120, // Hack - this semms to be in UTC?? + httpOnly: true + }) + } + else { + res.cookie('is_rest', "false", { + maxAge: 120000 * 120, // Hack - this semms to be in UTC?? + httpOnly: true + }) + if (!req.query.callback || req.query.callback === "") { + res.status(400).send({ + message: 'No callback parameter' + }) + return + } + res.cookie('auth_cb', req.query.callback, { + maxAge: 120000 * 120, // Hack - this semms to be in UTC?? + httpOnly: true + }) + } + } + + + /** + * Informs the client in the right mode about a successfull login and the token + * (clients choise is restored from cookies). + * @param {*} req Request object. + * @param {*} res Response object. + */ + async handleLoginSuccessBasedOnCookie(req, res) { + const data_json = await this.getUserLoginResponsePayload(req.user) + + if (req.cookies.is_rest && req.cookies.is_rest === "true") { + // REST request - send data + res.json(data_json) + } + else { + // UI request -> check if we have a callback url + // TODO: do we want to somehow limit the values for auth_cb? + if (req.cookies.auth_cb && req.cookies.auth_cb.startsWith("http")) { + // UI request -> redirect + res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`) + } + else { + res.status(400).send("No callback or already expired") + } + } + } + /** * Creates all (express) routes required for authentication. * @param {express.Router} router @@ -120,66 +177,27 @@ class Auth { // google-oauth20 strategy login route (this redirects to the google login) router.get('/auth/google', (req, res, next) => { const auth_func = passport.authenticate('google', { scope: ['email'] }) - if (!req.query.callback || req.query.callback === "") { - res.status(400).send({ - message: 'No callback parameter' - }) - return - } - res.cookie('auth_cb', req.query.callback, { - maxAge: 120000 * 120, // Hack - this semms to be in UTC?? - httpOnly: true - }) - auth_func(req, res, next); + this.paramsToCookies(req, res) + auth_func(req, res, next) }) // google-oauth20 strategy callback route (this receives the token from google) router.get('/auth/google/callback', passport.authenticate('google'), - (async function (req, res) { - // return the user login response json if the login was successfull - var data_json = await this.getUserLoginResponsePayload(req.user) - // res.json(data_json) - // TODO: do we want to somehow limit the values for auth_cb? - if (req.cookies.auth_cb) { - res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`) - } - else { - res.status(400).send("No callback or already expired") - } - }).bind(this) + this.handleLoginSuccessBasedOnCookie.bind(this) ) // openid strategy login route (this redirects to the configured openid login provider) router.get('/auth/openid', (req, res, next) => { const auth_func = passport.authenticate('openidconnect') - if (!req.query.callback || req.query.callback === "") { - res.status(400).send({ - message: 'No callback parameter' - }) - return - } - res.cookie('auth_cb', req.query.callback, { - maxAge: 120000 * 120, // Hack - this semms to be in UTC?? - httpOnly: true - }) - auth_func(req, res, next); + this.paramsToCookies(req, res) + auth_func(req, res, next) }) // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', passport.authenticate('openidconnect'), - (async function (req, res) { - // return the user login response json if the login was successfull - var data_json = await this.getUserLoginResponsePayload(req.user) - // res.json(data_json) - if (req.cookies.auth_cb) { - res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`) - } - else { - res.status(400).send("No callback or already expired") - } - }).bind(this) + this.handleLoginSuccessBasedOnCookie.bind(this) ) // Logout route From 2c90bba774ac107e4bdaa38248803c90083cde3c Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Wed, 20 Sep 2023 18:37:55 +0100 Subject: [PATCH 14/39] small refactorings --- server/Auth.js | 57 ++++++++++++++++++++++++++++----------- server/Server.js | 7 +++-- server/SocketAuthority.js | 29 +++++--------------- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 17fea247..d68ef876 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -17,10 +17,10 @@ class Auth { } /** - * Inializes all passportjs stragegies and other passportjs ralated initialization. + * Inializes all passportjs strategies and other passportjs ralated initialization. */ async initPassportJs() { - // Check if we should load the local strategy + // Check if we should load the local strategy (username + password login) if (global.ServerSettings.authActiveAuthMethods.includes("local")) { passport.use(new LocalStrategy(this.localAuthCheckUserPw.bind(this))) } @@ -33,13 +33,17 @@ class Auth { callbackURL: global.ServerSettings.authGoogleOauth20CallbackURL }, (async function (accessToken, refreshToken, profile, done) { // TODO: do we want to create the users which does not exist? + + // get user by email const user = await Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) if (!user || !user.isActive) { + // deny login done(null, null) return } + // permit login return done(null, user) }).bind(this))) } @@ -59,17 +63,23 @@ class Auth { }, (function (issuer, profile, done) { // TODO: do we want to create the users which does not exist? + + // get user by email var user = Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) if (!user || !user.isActive) { + // deny login done(null, null) return } + // permit login return done(null, user) }).bind(this))) } + // should be already initialied here - but ci had some problems so check again + // token is required to encrypt/protect the info in jwts if (!global.ServerSettings.tokenSecret) { await this.initTokenSecret() } @@ -83,22 +93,19 @@ class Auth { // define how to seralize a user (to be put into the session) passport.serializeUser(function (user, cb) { process.nextTick(function () { - // only store username and id to session - // TODO: do we want to store more info in the session? + // only store id to session return cb(null, JSON.stringify({ - "username": user.username, "id": user.id, - "email": user.email, })) }) }) - // define how to deseralize a user (use the username to get it from the database) + // define how to deseralize a user (use the ID to get it from the database) passport.deserializeUser((function (user, cb) { process.nextTick((async function () { const parsedUserInfo = JSON.parse(user) - // TODO: do the matching on username or better on id? - const dbUser = await Database.userModel.getUserByUsername(parsedUserInfo.username.toLowerCase()) + // load the user by ID that is stored in the session + const dbUser = await Database.userModel.getUserById(parsedUserInfo.id) return cb(null, dbUser) }).bind(this)) }).bind(this)) @@ -110,23 +117,28 @@ class Auth { * @param {*} res Response object. */ paramsToCookies(req, res) { - if (req.query.isRest && (req.query.isRest.toLowerCase() == "true" || req.query.isRest.toLowerCase() == "false")) { + if (req.query.isRest && req.query.isRest.toLowerCase() == "true") { + // store the isRest flag to the is_rest cookie res.cookie('is_rest', req.query.isRest.toLowerCase(), { maxAge: 120000 * 120, // Hack - this semms to be in UTC?? httpOnly: true }) } else { + // no isRest-flag set -> set is_rest cookie to false res.cookie('is_rest', "false", { maxAge: 120000 * 120, // Hack - this semms to be in UTC?? httpOnly: true }) + + // check if we are missing a callback parameter - we need one if isRest=false if (!req.query.callback || req.query.callback === "") { res.status(400).send({ message: 'No callback parameter' }) return } + // store the callback url to the auth_cb cookie res.cookie('auth_cb', req.query.callback, { maxAge: 120000 * 120, // Hack - this semms to be in UTC?? httpOnly: true @@ -142,6 +154,7 @@ class Auth { * @param {*} res Response object. */ async handleLoginSuccessBasedOnCookie(req, res) { + // get userLogin json (information about the user, server and the session) const data_json = await this.getUserLoginResponsePayload(req.user) if (req.cookies.is_rest && req.cookies.is_rest === "true") { @@ -152,7 +165,7 @@ class Auth { // UI request -> check if we have a callback url // TODO: do we want to somehow limit the values for auth_cb? if (req.cookies.auth_cb && req.cookies.auth_cb.startsWith("http")) { - // UI request -> redirect + // UI request -> redirect to auth_cb url and send the jwt token as parameter res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`) } else { @@ -165,7 +178,7 @@ class Auth { * Creates all (express) routes required for authentication. * @param {express.Router} router */ - initAuthRoutes(router) { + async initAuthRoutes(router) { // Local strategy login route (takes username and password) router.post('/login', passport.authenticate('local'), (async function (req, res) { @@ -177,6 +190,7 @@ class Auth { // google-oauth20 strategy login route (this redirects to the google login) router.get('/auth/google', (req, res, next) => { const auth_func = passport.authenticate('google', { scope: ['email'] }) + // params (isRest, callback) to a cookie that will be send to the client this.paramsToCookies(req, res) auth_func(req, res, next) }) @@ -184,12 +198,14 @@ class Auth { // google-oauth20 strategy callback route (this receives the token from google) router.get('/auth/google/callback', passport.authenticate('google'), + // on a successfull login: read the cookies and react like the client requested (callback or json) this.handleLoginSuccessBasedOnCookie.bind(this) ) // openid strategy login route (this redirects to the configured openid login provider) router.get('/auth/openid', (req, res, next) => { const auth_func = passport.authenticate('openidconnect') + // params (isRest, callback) to a cookie that will be send to the client this.paramsToCookies(req, res) auth_func(req, res, next) }) @@ -197,6 +213,7 @@ class Auth { // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', passport.authenticate('openidconnect'), + // on a successfull login: read the cookies and react like the client requested (callback or json) this.handleLoginSuccessBasedOnCookie.bind(this) ) @@ -239,7 +256,7 @@ class Auth { } /** - * Function to generate a jwt token for a given user. + * Function to validate a jwt token for a given user. * @param {string} token * @returns the tokens data. */ @@ -253,7 +270,7 @@ class Auth { } /** - * Generate a token for each user. + * Generate a token which is used to encrpt/protect the jwts. */ async initTokenSecret() { if (process.env.TOKEN_SECRET) { // User can supply their own token secret @@ -279,12 +296,15 @@ class Auth { * @param {function} done */ jwtAuthCheck(jwt_payload, done) { - const user = Database.userModel.getUserByUsername(jwt_payload.username.toLowerCase()) + // load user by id from the jwt token + const user = Database.userModel.getUserById(jwt_payload.id) if (!user || !user.isActive) { + // deny login done(null, null) return } + // approve login done(null, user) return } @@ -296,6 +316,7 @@ class Auth { * @param {function} done */ async localAuthCheckUserPw(username, password, done) { + // Load the user given it's username const user = await Database.userModel.getUserByUsername(username.toLowerCase()) if (!user || !user.isActive) { @@ -306,9 +327,11 @@ class Auth { // Check passwordless root user if (user.id === 'root' && (!user.pash || user.pash === '')) { if (password) { + // deny login done(null, null) return } + // approve login done(null, user) return } @@ -316,9 +339,11 @@ class Auth { // Check password match const compare = await bcrypt.compare(password, user.pash) if (compare) { + // approve login done(null, user) return } + // deny login done(null, null) return } @@ -343,7 +368,7 @@ class Auth { /** * Return the login info payload for a user. * @param {string} username - * @returns {string} jsonPayload + * @returns {Promise} jsonPayload */ async getUserLoginResponsePayload(user) { const libraryIds = await Database.libraryModel.getAllLibraryIds() diff --git a/server/Server.js b/server/Server.js index 7cc6fa76..cf55061d 100644 --- a/server/Server.js +++ b/server/Server.js @@ -87,6 +87,7 @@ class Server { } authMiddleware(req, res, next) { + // ask passportjs if the current request is authenticated this.auth.isAuthenticated(req, res, next) } @@ -145,7 +146,7 @@ class Server { resave: false, saveUninitialized: false, cookie: { - // also send the cookie if were hare not on https + // also send the cookie if were are not on https (not every use has https) secure: false }, })) @@ -155,8 +156,6 @@ class Server { app.use(passport.session()) // config passport.js await this.auth.initPassportJs() - // use auth on all routes - not used now - // app.use(passport.authenticate('session')) const router = express.Router() app.use(global.RouterBasePath, router) @@ -200,7 +199,7 @@ class Server { }) // Auth routes - this.auth.initAuthRoutes(router) + await this.auth.initAuthRoutes(router) // Client dynamic routes const dyanimicRoutes = [ diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index 81136ecf..28e59e40 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -2,8 +2,6 @@ const SocketIO = require('socket.io') const Logger = require('./Logger') const Database = require('./Database') const Auth = require('./Auth') -const passport = require('passport') -const expressSession = require('express-session') class SocketAuthority { constructor() { @@ -85,23 +83,6 @@ class SocketAuthority { } }) - /* - const wrap = middleware => (socket, next) => middleware(socket.request, {}, next); - - io.use(wrap(expressSession({ - secret: global.ServerSettings.tokenSecret, - resave: false, - saveUninitialized: false, - cookie: { - // also send the cookie if were hare not on https - secure: false - }, - }))); - - io.use(wrap(passport.initialize())); - io.use(wrap(passport.session())); - */ - this.io.on('connection', (socket) => { this.clients[socket.id] = { id: socket.id, @@ -168,14 +149,18 @@ class SocketAuthority { // When setting up a socket connection the user needs to be associated with a socket id // for this the client will send a 'auth' event that includes the users API token async authenticateSocket(socket, token) { - // TODO + // we don't use passport to authenticate the jwt we get over the socket connection. + // it's easier to directly verify/decode it. const token_data = Auth.validateAccessToken(token) - if (!token_data || !token_data.username) { + if (!token_data || !token_data.id) { + // Token invalid Logger.error('Cannot validate socket - invalid token') return socket.emit('invalid_token') } - const user = await Database.userModel.getUserByUsername(token_data.username) + // get the user via the id from the decoded jwt. + const user = await Database.userModel.getUserById(token_data.id) if (!user) { + // user not found Logger.error('Cannot validate socket - invalid token') return socket.emit('invalid_token') } From f6113e85c774314dd605e0500251c14c7834353f Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Wed, 20 Sep 2023 18:48:57 +0100 Subject: [PATCH 15/39] cookie lifetime --- server/Auth.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index d68ef876..332e500d 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -120,14 +120,14 @@ class Auth { if (req.query.isRest && req.query.isRest.toLowerCase() == "true") { // store the isRest flag to the is_rest cookie res.cookie('is_rest', req.query.isRest.toLowerCase(), { - maxAge: 120000 * 120, // Hack - this semms to be in UTC?? + maxAge: 120000, // 2 min httpOnly: true }) } else { // no isRest-flag set -> set is_rest cookie to false res.cookie('is_rest', "false", { - maxAge: 120000 * 120, // Hack - this semms to be in UTC?? + maxAge: 120000, // 2 min httpOnly: true }) @@ -140,7 +140,7 @@ class Auth { } // store the callback url to the auth_cb cookie res.cookie('auth_cb', req.query.callback, { - maxAge: 120000 * 120, // Hack - this semms to be in UTC?? + maxAge: 120000, // 2 min httpOnly: true }) } From 45cf00bd0498e7c10b39921a2133e67aae7a8b4e Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Wed, 20 Sep 2023 19:06:16 +0100 Subject: [PATCH 16/39] fix openid + jwt auth --- server/Auth.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 332e500d..f8074776 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -61,11 +61,11 @@ class Auth { scope: ["openid", "email", "profile"], skipUserProfile: false }, - (function (issuer, profile, done) { + (async function (issuer, profile, done) { // TODO: do we want to create the users which does not exist? // get user by email - var user = Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) + var user = await Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) if (!user || !user.isActive) { // deny login @@ -295,9 +295,9 @@ class Auth { * @param {Object} jwt_payload * @param {function} done */ - jwtAuthCheck(jwt_payload, done) { + async jwtAuthCheck(jwt_payload, done) { // load user by id from the jwt token - const user = Database.userModel.getUserById(jwt_payload.id) + const user = await Database.userModel.getUserById(jwt_payload.id) if (!user || !user.isActive) { // deny login From 2c25f64652366cb1b542e8a54f8dc5f096588357 Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Wed, 20 Sep 2023 19:16:08 +0100 Subject: [PATCH 17/39] Add /auth_methods route --- server/Auth.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/Auth.js b/server/Auth.js index f8074776..15be664c 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -228,6 +228,11 @@ class Auth { } }) }) + + // Get avilible auth methods + router.get('/auth_methods', (req, res) => { + res.json(global.ServerSettings.authActiveAuthMethods) + }) } /** From 0e75c8062725eab5d0445eec4d0a3c22bfa46bc5 Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Wed, 20 Sep 2023 19:45:32 +0100 Subject: [PATCH 18/39] prepare show/hide of login buttons --- client/pages/login.vue | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index beac44ff..29341ed3 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -27,7 +27,7 @@

{{ $strings.HeaderLogin }}

{{ error }}

-
+ @@ -39,10 +39,10 @@
@@ -222,9 +222,28 @@ export default { this.processing = false this.criticalError = 'Status check failed' }) + }, + async updateLoginVisibility() { + await this.$axios + .$get('/auth_methods') + .then((response) => { + ;['local', 'google-oauth20', 'openid'].forEach((auth_method) => { + debugger + if (response.includes(auth_method)) { + // TODO: show `#login-${auth_method}` + } else { + // TODO: hide `#login-${auth_method}` + } + }) + }) + .catch((error) => { + console.error('Failed', error.response) + return false + }) } }, async mounted() { + this.updateLoginVisibility() if (new URLSearchParams(window.location.search).get('setToken')) { localStorage.setItem('token', new URLSearchParams(window.location.search).get('setToken')) } From 7a131880e565da35c6bd774ca6d4b88a4092f5da Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Sat, 23 Sep 2023 17:00:14 +0100 Subject: [PATCH 19/39] show/hide of login buttons --- client/pages/login.vue | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index 29341ed3..f9d82087 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -27,7 +27,7 @@

{{ $strings.HeaderLogin }}

{{ error }}

-
+ @@ -39,10 +39,10 @@
@@ -70,7 +70,10 @@ export default { confirmPassword: '', ConfigPath: '', MetadataPath: '', - currentUrl: location.toString() + currentUrl: location.toString(), + login_local: true, + login_google_oauth20: false, + login_openid: false } }, watch: { @@ -227,14 +230,23 @@ export default { await this.$axios .$get('/auth_methods') .then((response) => { - ;['local', 'google-oauth20', 'openid'].forEach((auth_method) => { - debugger - if (response.includes(auth_method)) { - // TODO: show `#login-${auth_method}` - } else { - // TODO: hide `#login-${auth_method}` - } - }) + if (response.includes('local')) { + this.login_local = true + } else { + this.login_local = false + } + + if (response.includes('google-oauth20')) { + this.login_google_oauth20 = true + } else { + this.login_google_oauth20 = false + } + + if (response.includes('openid')) { + this.login_openid = true + } else { + this.login_openid = false + } }) .catch((error) => { console.error('Failed', error.response) @@ -243,7 +255,7 @@ export default { } }, async mounted() { - this.updateLoginVisibility() + this.$nextTick(async () => await this.updateLoginVisibility()) if (new URLSearchParams(window.location.search).get('setToken')) { localStorage.setItem('token', new URLSearchParams(window.location.search).get('setToken')) } From f42ab45e1b81dd3e95536b4996c147af8b31b8f4 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 23 Sep 2023 13:30:28 -0500 Subject: [PATCH 20/39] Update passwordless root user check to user user.type instead of user.id --- server/Auth.js | 10 ++-------- server/Server.js | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 15be664c..05044f74 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -78,16 +78,10 @@ class Auth { }).bind(this))) } - // should be already initialied here - but ci had some problems so check again - // token is required to encrypt/protect the info in jwts - if (!global.ServerSettings.tokenSecret) { - await this.initTokenSecret() - } - // Load the JwtStrategy (always) -> for bearer token auth passport.use(new JwtStrategy({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: global.ServerSettings.tokenSecret + secretOrKey: Database.serverSettings.tokenSecret }, this.jwtAuthCheck.bind(this))) // define how to seralize a user (to be put into the session) @@ -330,7 +324,7 @@ class Auth { } // Check passwordless root user - if (user.id === 'root' && (!user.pash || user.pash === '')) { + if (user.type === 'root' && (!user.pash || user.pash === '')) { if (password) { // deny login done(null, null) diff --git a/server/Server.js b/server/Server.js index cf55061d..2424456d 100644 --- a/server/Server.js +++ b/server/Server.js @@ -139,7 +139,7 @@ class Server { const app = express() // parse cookies in requests - app.use(cookieParser()); + app.use(cookieParser()) // enable express-session app.use(expressSession({ secret: global.ServerSettings.tokenSecret, From 9922294507f6152f15f801ff713492d16c5ff256 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 23 Sep 2023 13:42:28 -0500 Subject: [PATCH 21/39] Fix setting tokenSecret on init --- server/Auth.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 05044f74..13d7cde9 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -273,9 +273,9 @@ class Auth { */ async initTokenSecret() { if (process.env.TOKEN_SECRET) { // User can supply their own token secret - global.ServerSettings.tokenSecret = process.env.TOKEN_SECRET + Database.serverSettings.tokenSecret = process.env.TOKEN_SECRET } else { - global.ServerSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64') + Database.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64') } await Database.updateServerSettings() From f6de373388f2065e8c15c0d279fcaaae2bee1fce Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 24 Sep 2023 12:36:36 -0500 Subject: [PATCH 22/39] Update /status endpoint to return available auth methods, fix socket auth, update openid to use username instead of email --- client/pages/login.vue | 69 ++++++++++++++++++++------------------- server/Auth.js | 59 ++++++++++++++++----------------- server/Server.js | 3 +- server/SocketAuthority.js | 15 ++++++--- 4 files changed, 76 insertions(+), 70 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index f9d82087..73cd2767 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -25,8 +25,11 @@

{{ $strings.HeaderLogin }}

+
+

{{ error }}

+
@@ -37,7 +40,9 @@ {{ processing ? 'Checking...' : $strings.ButtonSubmit }}
-
+ +
+
Login with Google @@ -106,6 +111,9 @@ export default { computed: { user() { return this.$store.state.user.user + }, + googleAuthUri() { + return `${process.env.serverUrl}/auth/openid?callback=${currentUrl}` } }, methods: { @@ -210,14 +218,16 @@ export default { this.processing = true this.$axios .$get('/status') - .then((res) => { + .then((data) => { this.processing = false - this.isInit = res.isInit - this.showInitScreen = !res.isInit - this.$setServerLanguageCode(res.language) + this.isInit = data.isInit + this.showInitScreen = !data.isInit + this.$setServerLanguageCode(data.language) if (this.showInitScreen) { - this.ConfigPath = res.ConfigPath || '' - this.MetadataPath = res.MetadataPath || '' + this.ConfigPath = data.ConfigPath || '' + this.MetadataPath = data.MetadataPath || '' + } else { + this.updateLoginVisibility(data.authMethods || []) } }) .catch((error) => { @@ -226,43 +236,34 @@ export default { this.criticalError = 'Status check failed' }) }, - async updateLoginVisibility() { - await this.$axios - .$get('/auth_methods') - .then((response) => { - if (response.includes('local')) { - this.login_local = true - } else { - this.login_local = false - } + updateLoginVisibility(authMethods) { + if (authMethods.includes('local') || !authMethods.length) { + this.login_local = true + } else { + this.login_local = false + } - if (response.includes('google-oauth20')) { - this.login_google_oauth20 = true - } else { - this.login_google_oauth20 = false - } + if (authMethods.includes('google-oauth20')) { + this.login_google_oauth20 = true + } else { + this.login_google_oauth20 = false + } - if (response.includes('openid')) { - this.login_openid = true - } else { - this.login_openid = false - } - }) - .catch((error) => { - console.error('Failed', error.response) - return false - }) + if (authMethods.includes('openid')) { + this.login_openid = true + } else { + this.login_openid = false + } } }, async mounted() { - this.$nextTick(async () => await this.updateLoginVisibility()) if (new URLSearchParams(window.location.search).get('setToken')) { localStorage.setItem('token', new URLSearchParams(window.location.search).get('setToken')) } if (localStorage.getItem('token')) { - var userfound = await this.checkAuth() - if (userfound) return // if valid user no need to check status + if (await this.checkAuth()) return // if valid user no need to check status } + this.checkStatus() } } diff --git a/server/Auth.js b/server/Auth.js index 13d7cde9..0041fbed 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -64,10 +64,9 @@ class Auth { (async function (issuer, profile, done) { // TODO: do we want to create the users which does not exist? - // get user by email - var user = await Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) + const user = await Database.userModel.getUserByUsername(profile.username) - if (!user || !user.isActive) { + if (!user?.isActive) { // deny login done(null, null) return @@ -106,9 +105,10 @@ class Auth { } /** - * Stores the client's choise how the login callback should happen in temp cookies. - * @param {*} req Request object. - * @param {*} res Response object. + * Stores the client's choice how the login callback should happen in temp cookies + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ paramsToCookies(req, res) { if (req.query.isRest && req.query.isRest.toLowerCase() == "true") { @@ -140,12 +140,12 @@ class Auth { } } - /** * Informs the client in the right mode about a successfull login and the token * (clients choise is restored from cookies). - * @param {*} req Request object. - * @param {*} res Response object. + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ async handleLoginSuccessBasedOnCookie(req, res) { // get userLogin json (information about the user, server and the session) @@ -170,16 +170,15 @@ class Auth { /** * Creates all (express) routes required for authentication. - * @param {express.Router} router + * + * @param {import('express').Router} router */ async initAuthRoutes(router) { // Local strategy login route (takes username and password) - router.post('/login', passport.authenticate('local'), - (async function (req, res) { - // return the user login response json if the login was successfull - res.json(await this.getUserLoginResponsePayload(req.user)) - }).bind(this) - ) + router.post('/login', passport.authenticate('local'), async (req, res) => { + // return the user login response json if the login was successfull + res.json(await this.getUserLoginResponsePayload(req.user)) + }) // google-oauth20 strategy login route (this redirects to the google login) router.get('/auth/google', (req, res, next) => { @@ -222,18 +221,13 @@ class Auth { } }) }) - - // Get avilible auth methods - router.get('/auth_methods', (req, res) => { - res.json(global.ServerSettings.authActiveAuthMethods) - }) } /** * middleware to use in express to only allow authenticated users. - * @param {express.Request} req - * @param {express.Response} res - * @param {express.NextFunction} next + * @param {import('express').Request} req + * @param {import('express').Response} res + * @param {import('express').NextFunction} next */ isAuthenticated(req, res, next) { // check if session cookie says that we are authenticated @@ -246,18 +240,20 @@ class Auth { } /** - * Function to generate a jwt token for a given user. + * Function to generate a jwt token for a given user + * * @param {Object} user - * @returns the token. + * @returns {string} token */ generateAccessToken(user) { return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret) } /** - * Function to validate a jwt token for a given user. + * Function to validate a jwt token for a given user + * * @param {string} token - * @returns the tokens data. + * @returns {Object} tokens data */ static validateAccessToken(token) { try { @@ -365,9 +361,10 @@ class Auth { } /** - * Return the login info payload for a user. - * @param {string} username - * @returns {Promise} jsonPayload + * Return the login info payload for a user + * + * @param {Object} user + * @returns {Promise} jsonPayload */ async getUserLoginResponsePayload(user) { const libraryIds = await Database.libraryModel.getAllLibraryIds() diff --git a/server/Server.js b/server/Server.js index 2424456d..dbb9bddf 100644 --- a/server/Server.js +++ b/server/Server.js @@ -238,7 +238,8 @@ class Server { // server has been initialized if a root user exists const payload = { isInit: Database.hasRootUser, - language: Database.serverSettings.language + language: Database.serverSettings.language, + authMethods: Database.serverSettings.authActiveAuthMethods } if (!payload.isInit) { payload.ConfigPath = global.ConfigPath diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index 28e59e40..f1c69b24 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -146,24 +146,31 @@ class SocketAuthority { }) } - // When setting up a socket connection the user needs to be associated with a socket id - // for this the client will send a 'auth' event that includes the users API token + /** + * When setting up a socket connection the user needs to be associated with a socket id + * for this the client will send a 'auth' event that includes the users API token + * + * @param {SocketIO.Socket} socket + * @param {string} token JWT + */ async authenticateSocket(socket, token) { // we don't use passport to authenticate the jwt we get over the socket connection. // it's easier to directly verify/decode it. const token_data = Auth.validateAccessToken(token) - if (!token_data || !token_data.id) { + + if (!token_data?.userId) { // Token invalid Logger.error('Cannot validate socket - invalid token') return socket.emit('invalid_token') } // get the user via the id from the decoded jwt. - const user = await Database.userModel.getUserById(token_data.id) + const user = await Database.userModel.getUserByIdOrOldId(token_data.userId) if (!user) { // user not found Logger.error('Cannot validate socket - invalid token') return socket.emit('invalid_token') } + const client = this.clients[socket.id] if (!client) { Logger.error(`[SocketAuthority] Socket for user ${user.username} has no client`) From 7ba10db7d4f10788c515e139cc732fbb5402eb24 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 24 Sep 2023 12:39:38 -0500 Subject: [PATCH 23/39] Update login button openid and google urls --- client/pages/login.vue | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index 73cd2767..4ca7c302 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -44,10 +44,10 @@
@@ -75,7 +75,6 @@ export default { confirmPassword: '', ConfigPath: '', MetadataPath: '', - currentUrl: location.toString(), login_local: true, login_google_oauth20: false, login_openid: false @@ -113,7 +112,10 @@ export default { return this.$store.state.user.user }, googleAuthUri() { - return `${process.env.serverUrl}/auth/openid?callback=${currentUrl}` + return `${process.env.serverUrl}/auth/google?callback=${location.toString()}` + }, + openidAuthUri() { + return `${process.env.serverUrl}/auth/openid?callback=${location.toString()}` } }, methods: { From e282142d3fcf3d483cbea8432c85e216fed6ca72 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 24 Sep 2023 15:36:35 -0500 Subject: [PATCH 24/39] Add authentication page in config, add /auth-settings GET endpoint, remove authOpenIDCallbackURL server setting --- client/components/app/ConfigSideNav.vue | 5 + client/pages/config.vue | 1 + client/pages/config/authentication.vue | 138 ++++++++++++++++++++++ client/store/index.js | 2 +- client/strings/en-us.json | 1 + server/Auth.js | 46 ++++---- server/Server.js | 2 - server/controllers/MiscController.js | 23 +++- server/objects/settings/ServerSettings.js | 51 ++++++-- server/routers/ApiRouter.js | 1 + 10 files changed, 225 insertions(+), 45 deletions(-) create mode 100644 client/pages/config/authentication.vue diff --git a/client/components/app/ConfigSideNav.vue b/client/components/app/ConfigSideNav.vue index 50e440d7..677aba70 100644 --- a/client/components/app/ConfigSideNav.vue +++ b/client/components/app/ConfigSideNav.vue @@ -104,6 +104,11 @@ export default { id: 'config-rss-feeds', title: this.$strings.HeaderRSSFeeds, path: '/config/rss-feeds' + }, + { + id: 'config-authentication', + title: this.$strings.HeaderAuthentication, + path: '/config/authentication' } ] diff --git a/client/pages/config.vue b/client/pages/config.vue index 542b7f2c..fdbd7150 100644 --- a/client/pages/config.vue +++ b/client/pages/config.vue @@ -57,6 +57,7 @@ export default { else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils else if (pageName === 'rss-feeds') return this.$strings.HeaderRSSFeeds else if (pageName === 'email') return this.$strings.HeaderEmail + else if (pageName === 'authentication') return this.$strings.HeaderAuthentication } return this.$strings.HeaderSettings } diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue new file mode 100644 index 00000000..acc0ac13 --- /dev/null +++ b/client/pages/config/authentication.vue @@ -0,0 +1,138 @@ + + + + diff --git a/client/store/index.js b/client/store/index.js index 2f8201c1..ed7c35b6 100644 --- a/client/store/index.js +++ b/client/store/index.js @@ -66,7 +66,7 @@ export const getters = { export const actions = { updateServerSettings({ commit }, payload) { - var updatePayload = { + const updatePayload = { ...payload } return this.$axios.$patch('/api/settings', updatePayload).then((result) => { diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 75606da2..47cfe448 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -88,6 +88,7 @@ "HeaderAppriseNotificationSettings": "Apprise Notification Settings", "HeaderAudiobookTools": "Audiobook File Management Tools", "HeaderAudioTracks": "Audio Tracks", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Backups", "HeaderChangePassword": "Change Password", "HeaderChapters": "Chapters", diff --git a/server/Auth.js b/server/Auth.js index 0041fbed..d6d67d49 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -57,24 +57,23 @@ class Auth { userInfoURL: global.ServerSettings.authOpenIDUserInfoURL, clientID: global.ServerSettings.authOpenIDClientID, clientSecret: global.ServerSettings.authOpenIDClientSecret, - callbackURL: global.ServerSettings.authOpenIDCallbackURL, + callbackURL: '/auth/openid/callback', scope: ["openid", "email", "profile"], skipUserProfile: false - }, - (async function (issuer, profile, done) { - // TODO: do we want to create the users which does not exist? + }, async (issuer, profile, done) => { + // TODO: do we want to create the users which does not exist? - const user = await Database.userModel.getUserByUsername(profile.username) + const user = await Database.userModel.getUserByUsername(profile.username) - if (!user?.isActive) { - // deny login - done(null, null) - return - } + if (!user?.isActive) { + // deny login + done(null, null) + return + } - // permit login - return done(null, user) - }).bind(this))) + // permit login + return done(null, user) + })) } // Load the JwtStrategy (always) -> for bearer token auth @@ -111,14 +110,13 @@ class Auth { * @param {import('express').Response} res */ paramsToCookies(req, res) { - if (req.query.isRest && req.query.isRest.toLowerCase() == "true") { + if (req.query.isRest?.toLowerCase() == "true") { // store the isRest flag to the is_rest cookie res.cookie('is_rest', req.query.isRest.toLowerCase(), { maxAge: 120000, // 2 min httpOnly: true }) - } - else { + } else { // no isRest-flag set -> set is_rest cookie to false res.cookie('is_rest', "false", { maxAge: 120000, // 2 min @@ -126,7 +124,7 @@ class Auth { }) // check if we are missing a callback parameter - we need one if isRest=false - if (!req.query.callback || req.query.callback === "") { + if (!req.query.callback) { res.status(400).send({ message: 'No callback parameter' }) @@ -151,19 +149,17 @@ class Auth { // get userLogin json (information about the user, server and the session) const data_json = await this.getUserLoginResponsePayload(req.user) - if (req.cookies.is_rest && req.cookies.is_rest === "true") { + if (req.cookies.is_rest === 'true') { // REST request - send data res.json(data_json) - } - else { + } else { // UI request -> check if we have a callback url // TODO: do we want to somehow limit the values for auth_cb? - if (req.cookies.auth_cb && req.cookies.auth_cb.startsWith("http")) { + if (req.cookies.auth_cb?.startsWith('http')) { // UI request -> redirect to auth_cb url and send the jwt token as parameter res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`) - } - else { - res.status(400).send("No callback or already expired") + } else { + res.status(400).send('No callback or already expired') } } } @@ -205,7 +201,7 @@ class Auth { // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', - passport.authenticate('openidconnect'), + passport.authenticate('openidconnect', { failureRedirect: '/login', failureMessage: true }), // on a successfull login: read the cookies and react like the client requested (callback or json) this.handleLoginSuccessBasedOnCookie.bind(this) ) diff --git a/server/Server.js b/server/Server.js index dbb9bddf..08f4b8d9 100644 --- a/server/Server.js +++ b/server/Server.js @@ -163,8 +163,6 @@ class Server { this.server = http.createServer(app) - - router.use(fileUpload({ defCharset: 'utf8', defParamCharset: 'utf8', diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 0fa1c62f..d1f3686b 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -117,8 +117,9 @@ class MiscController { /** * PATCH: /api/settings * Update server settings - * @param {*} req - * @param {*} res + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ async updateServerSettings(req, res) { if (!req.user.isAdminOrUp) { @@ -246,8 +247,8 @@ class MiscController { * POST: /api/authorize * Used to authorize an API token * - * @param {*} req - * @param {*} res + * @param {import('express').Request} req + * @param {import('express').Response} res */ async authorize(req, res) { if (!req.user) { @@ -539,5 +540,19 @@ class MiscController { res.status(400).send(error.message) } } + + /** + * GET: api/auth-settings (admin only) + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + getAuthSettings(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get auth settings`) + return res.sendStatus(403) + } + return res.json(Database.serverSettings.authenticationSettings) + } } module.exports = new MiscController() \ No newline at end of file diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 71358a00..9348d691 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -64,14 +64,13 @@ class ServerSettings { this.authGoogleOauth20ClientSecret = '' this.authGoogleOauth20CallbackURL = '' - // generic-oauth20 settings + // openid settings this.authOpenIDIssuerURL = '' this.authOpenIDAuthorizationURL = '' this.authOpenIDTokenURL = '' this.authOpenIDUserInfoURL = '' this.authOpenIDClientID = '' this.authOpenIDClientSecret = '' - this.authOpenIDCallbackURL = '' if (settings) { this.construct(settings) @@ -126,7 +125,6 @@ class ServerSettings { this.authOpenIDUserInfoURL = settings.authOpenIDUserInfoURL || '' this.authOpenIDClientID = settings.authOpenIDClientID || '' this.authOpenIDClientSecret = settings.authOpenIDClientSecret || '' - this.authOpenIDCallbackURL = settings.authOpenIDCallbackURL || '' if (!Array.isArray(this.authActiveAuthMethods)) { this.authActiveAuthMethods = ['local'] @@ -144,16 +142,15 @@ class ServerSettings { // remove uninitialized methods // OpenID - if (this.authActiveAuthMethods.includes('generic-oauth20') && ( + if (this.authActiveAuthMethods.includes('openid') && ( this.authOpenIDIssuerURL === '' || this.authOpenIDAuthorizationURL === '' || this.authOpenIDTokenURL === '' || this.authOpenIDUserInfoURL === '' || this.authOpenIDClientID === '' || - this.authOpenIDClientSecret === '' || - this.authOpenIDCallbackURL === '' + this.authOpenIDClientSecret === '' )) { - this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('generic-oauth20', 0), 1) + this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('openid', 0), 1) } // fallback to local @@ -228,8 +225,7 @@ class ServerSettings { authOpenIDTokenURL: this.authOpenIDTokenURL, authOpenIDUserInfoURL: this.authOpenIDUserInfoURL, authOpenIDClientID: this.authOpenIDClientID, // Do not return to client - authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client - authOpenIDCallbackURL: this.authOpenIDCallbackURL + authOpenIDClientSecret: this.authOpenIDClientSecret // Do not return to client } } @@ -243,13 +239,42 @@ class ServerSettings { return json } + get authenticationSettings() { + return { + authActiveAuthMethods: this.authActiveAuthMethods, + authGoogleOauth20ClientID: this.authGoogleOauth20ClientID, // Do not return to client + authGoogleOauth20ClientSecret: this.authGoogleOauth20ClientSecret, // Do not return to client + authGoogleOauth20CallbackURL: this.authGoogleOauth20CallbackURL, + authOpenIDIssuerURL: this.authOpenIDIssuerURL, + authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL, + authOpenIDTokenURL: this.authOpenIDTokenURL, + authOpenIDUserInfoURL: this.authOpenIDUserInfoURL, + authOpenIDClientID: this.authOpenIDClientID, // Do not return to client + authOpenIDClientSecret: this.authOpenIDClientSecret // Do not return to client + } + } + + /** + * Update server settings + * + * @param {Object} payload + * @returns {boolean} true if updates were made + */ update(payload) { let hasUpdates = false for (const key in payload) { - if (key === 'sortingPrefixes' && payload[key] && payload[key].length) { - const prefixesCleaned = payload[key].filter(prefix => !!prefix).map(prefix => prefix.toLowerCase()) - if (prefixesCleaned.join(',') !== this[key].join(',')) { - this[key] = [...prefixesCleaned] + if (key === 'sortingPrefixes') { + // Sorting prefixes are updated with the /api/sorting-prefixes endpoint + continue + } else if (key === 'authActiveAuthMethods') { + if (!payload[key]?.length) { + Logger.error(`[ServerSettings] Invalid authActiveAuthMethods`, payload[key]) + continue + } + this.authActiveAuthMethods.sort() + payload[key].sort() + if (payload[key].join() !== this.authActiveAuthMethods.join()) { + this.authActiveAuthMethods = payload[key] hasUpdates = true } } else if (this[key] !== payload[key]) { diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 71d9429e..d91c9312 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -306,6 +306,7 @@ class ApiRouter { this.router.post('/genres/rename', MiscController.renameGenre.bind(this)) this.router.delete('/genres/:genre', MiscController.deleteGenre.bind(this)) this.router.post('/validate-cron', MiscController.validateCronExpression.bind(this)) + this.router.get('/auth-settings', MiscController.getAuthSettings.bind(this)) } async getDirectories(dir, relpath, excludedDirs, level = 0) { From 0d5a30b21440ad0736332a976cb43eba264112c9 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 25 Sep 2023 17:05:58 -0500 Subject: [PATCH 25/39] Update JWT auth extractors, add state in openid redirect, add back cors for api router --- server/Auth.js | 42 +++++++++++++++++++------ server/Server.js | 2 +- server/controllers/SessionController.js | 20 ++++++------ 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index d6d67d49..b7ea59c4 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -16,6 +16,18 @@ class Auth { constructor() { } + static cors(req, res, next) { + res.header('Access-Control-Allow-Origin', '*') + res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS') + res.header('Access-Control-Allow-Headers', '*') + res.header('Access-Control-Allow-Credentials', true) + if (req.method === 'OPTIONS') { + res.sendStatus(200) + } else { + next() + } + } + /** * Inializes all passportjs strategies and other passportjs ralated initialization. */ @@ -78,7 +90,7 @@ class Auth { // Load the JwtStrategy (always) -> for bearer token auth passport.use(new JwtStrategy({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), ExtractJwt.fromUrlQueryParameter('token')]), secretOrKey: Database.serverSettings.tokenSecret }, this.jwtAuthCheck.bind(this))) @@ -123,15 +135,25 @@ class Auth { httpOnly: true }) + // persist state if passed in + if (req.query.state) { + res.cookie('auth_state', req.query.state, { + maxAge: 120000, // 2 min + httpOnly: true + }) + } + + const callback = req.query.redirect_uri || req.query.callback + // check if we are missing a callback parameter - we need one if isRest=false - if (!req.query.callback) { + if (!callback) { res.status(400).send({ message: 'No callback parameter' }) return } // store the callback url to the auth_cb cookie - res.cookie('auth_cb', req.query.callback, { + res.cookie('auth_cb', callback, { maxAge: 120000, // 2 min httpOnly: true }) @@ -155,9 +177,10 @@ class Auth { } else { // UI request -> check if we have a callback url // TODO: do we want to somehow limit the values for auth_cb? - if (req.cookies.auth_cb?.startsWith('http')) { + if (req.cookies.auth_cb) { + let stateQuery = req.cookies.auth_state ? `&state=${req.cookies.auth_state}` : '' // UI request -> redirect to auth_cb url and send the jwt token as parameter - res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`) + res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}${stateQuery}`) } else { res.status(400).send('No callback or already expired') } @@ -201,10 +224,9 @@ class Auth { // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', - passport.authenticate('openidconnect', { failureRedirect: '/login', failureMessage: true }), + passport.authenticate('openidconnect'), // on a successfull login: read the cookies and react like the client requested (callback or json) - this.handleLoginSuccessBasedOnCookie.bind(this) - ) + this.handleLoginSuccessBasedOnCookie.bind(this)) // Logout route router.post('/logout', (req, res) => { @@ -288,9 +310,9 @@ class Auth { */ async jwtAuthCheck(jwt_payload, done) { // load user by id from the jwt token - const user = await Database.userModel.getUserById(jwt_payload.id) + const user = await Database.userModel.getUserByIdOrOldId(jwt_payload.userId) - if (!user || !user.isActive) { + if (!user?.isActive) { // deny login done(null, null) return diff --git a/server/Server.js b/server/Server.js index 08f4b8d9..2f04b850 100644 --- a/server/Server.js +++ b/server/Server.js @@ -180,7 +180,7 @@ class Server { router.use(express.static(Path.join(global.appRoot, 'static'))) // router.use('/api/v1', routes) // TODO: New routes - router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router) + router.use('/api', Auth.cors, this.authMiddleware.bind(this), this.apiRouter.router) router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router) // RSS Feed temp route diff --git a/server/controllers/SessionController.js b/server/controllers/SessionController.js index 85baeb27..884f0cd6 100644 --- a/server/controllers/SessionController.js +++ b/server/controllers/SessionController.js @@ -6,7 +6,7 @@ class SessionController { constructor() { } async findOne(req, res) { - return res.json(req.session) + return res.json(req.playbackSession) } async getAllWithUserData(req, res) { @@ -63,32 +63,32 @@ class SessionController { } async getOpenSession(req, res) { - const libraryItem = await Database.libraryItemModel.getOldById(req.session.libraryItemId) - const sessionForClient = req.session.toJSONForClient(libraryItem) + const libraryItem = await Database.libraryItemModel.getOldById(req.playbackSession.libraryItemId) + const sessionForClient = req.playbackSession.toJSONForClient(libraryItem) res.json(sessionForClient) } // POST: api/session/:id/sync sync(req, res) { - this.playbackSessionManager.syncSessionRequest(req.user, req.session, req.body, res) + this.playbackSessionManager.syncSessionRequest(req.user, req.playbackSession, req.body, res) } // POST: api/session/:id/close close(req, res) { let syncData = req.body if (syncData && !Object.keys(syncData).length) syncData = null - this.playbackSessionManager.closeSessionRequest(req.user, req.session, syncData, res) + this.playbackSessionManager.closeSessionRequest(req.user, req.playbackSession, syncData, res) } // DELETE: api/session/:id async delete(req, res) { // if session is open then remove it - const openSession = this.playbackSessionManager.getSession(req.session.id) + const openSession = this.playbackSessionManager.getSession(req.playbackSession.id) if (openSession) { - await this.playbackSessionManager.removeSession(req.session.id) + await this.playbackSessionManager.removeSession(req.playbackSession.id) } - await Database.removePlaybackSession(req.session.id) + await Database.removePlaybackSession(req.playbackSession.id) res.sendStatus(200) } @@ -111,7 +111,7 @@ class SessionController { return res.sendStatus(404) } - req.session = playbackSession + req.playbackSession = playbackSession next() } @@ -130,7 +130,7 @@ class SessionController { return res.sendStatus(403) } - req.session = playbackSession + req.playbackSession = playbackSession next() } } From 828b96b2d90eaabc5cb3574ce0b68cee9ec1944e Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 2 Nov 2023 13:55:01 -0500 Subject: [PATCH 26/39] Add server settings for changing openid button text and auto launching openid --- client/pages/config/authentication.vue | 8 ++++++++ client/pages/login.vue | 19 +++++++++++++---- server/Server.js | 3 ++- server/objects/settings/ServerSettings.js | 25 +++++++++++++++++++---- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index acc0ac13..317364a2 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -26,6 +26,14 @@ + + + +
+ +

Auto Launch

+

Redirect to the auth provider automatically when navigating to the /login page

+
diff --git a/client/pages/login.vue b/client/pages/login.vue index 4ca7c302..724f5999 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -48,7 +48,7 @@ Login with Google - Login with OpenId + {{ openIDButtonText }} @@ -77,7 +77,8 @@ export default { MetadataPath: '', login_local: true, login_google_oauth20: false, - login_openid: false + login_openid: false, + authFormData: null } }, watch: { @@ -116,6 +117,9 @@ export default { }, openidAuthUri() { return `${process.env.serverUrl}/auth/openid?callback=${location.toString()}` + }, + openIDButtonText() { + return this.authFormData?.authOpenIDButtonText || 'Login with OpenId' } }, methods: { @@ -221,7 +225,6 @@ export default { this.$axios .$get('/status') .then((data) => { - this.processing = false this.isInit = data.isInit this.showInitScreen = !data.isInit this.$setServerLanguageCode(data.language) @@ -229,14 +232,17 @@ export default { this.ConfigPath = data.ConfigPath || '' this.MetadataPath = data.MetadataPath || '' } else { + this.authFormData = data.authFormData this.updateLoginVisibility(data.authMethods || []) } }) .catch((error) => { console.error('Status check failed', error) - this.processing = false this.criticalError = 'Status check failed' }) + .finally(() => { + this.processing = false + }) }, updateLoginVisibility(authMethods) { if (authMethods.includes('local') || !authMethods.length) { @@ -252,6 +258,11 @@ export default { } if (authMethods.includes('openid')) { + // Auto redirect unless query string ?autoLaunch=0 + if (this.authFormData?.authOpenIDAutoLaunch && this.$route.query?.autoLaunch !== '0') { + window.location.href = this.openidAuthUri + } + this.login_openid = true } else { this.login_openid = false diff --git a/server/Server.js b/server/Server.js index 08f206dc..6c6a17b0 100644 --- a/server/Server.js +++ b/server/Server.js @@ -230,7 +230,8 @@ class Server { const payload = { isInit: Database.hasRootUser, language: Database.serverSettings.language, - authMethods: Database.serverSettings.authActiveAuthMethods + authMethods: Database.serverSettings.authActiveAuthMethods, + authFormData: Database.serverSettings.authFormData } if (!payload.isInit) { payload.ConfigPath = global.ConfigPath diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 301e38e4..71e2e05d 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -70,6 +70,8 @@ class ServerSettings { this.authOpenIDUserInfoURL = '' this.authOpenIDClientID = '' this.authOpenIDClientSecret = '' + this.authOpenIDButtonText = 'Login with OpenId' + this.authOpenIDAutoLaunch = false if (settings) { this.construct(settings) @@ -122,12 +124,14 @@ class ServerSettings { this.authOpenIDUserInfoURL = settings.authOpenIDUserInfoURL || '' this.authOpenIDClientID = settings.authOpenIDClientID || '' this.authOpenIDClientSecret = settings.authOpenIDClientSecret || '' + this.authOpenIDButtonText = settings.authOpenIDButtonText || 'Login with OpenId' + this.authOpenIDAutoLaunch = !!settings.authOpenIDAutoLaunch if (!Array.isArray(this.authActiveAuthMethods)) { this.authActiveAuthMethods = ['local'] } - // remove uninitialized methods + // remove uninitialized methods // GoogleOauth20 if (this.authActiveAuthMethods.includes('google-oauth20') && ( this.authGoogleOauth20ClientID === '' || @@ -137,7 +141,7 @@ class ServerSettings { this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('google-oauth20', 0), 1) } - // remove uninitialized methods + // remove uninitialized methods // OpenID if (this.authActiveAuthMethods.includes('openid') && ( this.authOpenIDIssuerURL === '' || @@ -221,7 +225,9 @@ class ServerSettings { authOpenIDTokenURL: this.authOpenIDTokenURL, authOpenIDUserInfoURL: this.authOpenIDUserInfoURL, authOpenIDClientID: this.authOpenIDClientID, // Do not return to client - authOpenIDClientSecret: this.authOpenIDClientSecret // Do not return to client + authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client + authOpenIDButtonText: this.authOpenIDButtonText, + authOpenIDAutoLaunch: this.authOpenIDAutoLaunch } } @@ -246,10 +252,21 @@ class ServerSettings { authOpenIDTokenURL: this.authOpenIDTokenURL, authOpenIDUserInfoURL: this.authOpenIDUserInfoURL, authOpenIDClientID: this.authOpenIDClientID, // Do not return to client - authOpenIDClientSecret: this.authOpenIDClientSecret // Do not return to client + authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client + authOpenIDButtonText: this.authOpenIDButtonText, + authOpenIDAutoLaunch: this.authOpenIDAutoLaunch } } + get authFormData() { + const clientFormData = {} + if (this.authActiveAuthMethods.includes('openid')) { + clientFormData.authOpenIDButtonText = this.authOpenIDButtonText + clientFormData.authOpenIDAutoLaunch = this.authOpenIDAutoLaunch + } + return clientFormData + } + /** * Update server settings * From 840811b46460d08690dd59c24dcc24daafb2587f Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 4 Nov 2023 15:36:43 -0500 Subject: [PATCH 27/39] Replace passport openidconnect plugin with openid-client, add JWKS and logout URL server settings, use email and email_verified instead of username --- client/pages/config/authentication.vue | 8 ++ package-lock.json | 91 ++++++++++++++++------- package.json | 2 +- server/Auth.js | 59 ++++++++++----- server/objects/settings/ServerSettings.js | 9 +++ 5 files changed, 125 insertions(+), 44 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 317364a2..13867ef3 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -23,6 +23,10 @@ + + + + @@ -97,6 +101,10 @@ export default { this.$toast.error('Userinfo URL required') isValid = false } + if (!this.newAuthSettings.authOpenIDJwksURL) { + this.$toast.error('JWKS URL required') + isValid = false + } if (!this.newAuthSettings.authOpenIDClientID) { this.$toast.error('Client ID required') isValid = false diff --git a/package-lock.json b/package-lock.json index 55149176..dd2b8339 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,10 +17,10 @@ "htmlparser2": "^8.0.1", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", + "openid-client": "^5.6.1", "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", - "passport-openidconnect": "^0.1.1", "sequelize": "^6.32.1", "socket.io": "^4.5.4", "sqlite3": "^5.1.6", @@ -1343,6 +1343,14 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "optional": true }, + "node_modules/jose": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", + "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", @@ -1883,6 +1891,14 @@ "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": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -1891,6 +1907,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -1918,6 +1942,20 @@ "wrappy": "1" } }, + "node_modules/openid-client": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.1.tgz", + "integrity": "sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==", + "dependencies": { + "jose": "^4.15.1", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -1997,22 +2035,6 @@ "url": "https://github.com/sponsors/jaredhanson" } }, - "node_modules/passport-openidconnect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.1.tgz", - "integrity": "sha512-r0QJiWEzwCg2MeCIXVP5G6YxVRqnEsZ2HpgKRthZ9AiQHJrgGUytXpsdcGF9BRwd3yMrEesb/uG/Yxb86rrY0g==", - "dependencies": { - "oauth": "0.9.x", - "passport-strategy": "1.x.x" - }, - "engines": { - "node": ">= 0.6.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jaredhanson" - } - }, "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -3929,6 +3951,11 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "optional": true }, + "jose": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", + "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==" + }, "jsonwebtoken": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", @@ -4330,11 +4357,21 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" + }, "object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" }, + "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==" + }, "on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -4356,6 +4393,17 @@ "wrappy": "1" } }, + "openid-client": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.1.tgz", + "integrity": "sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==", + "requires": { + "jose": "^4.15.1", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + } + }, "p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -4409,15 +4457,6 @@ "utils-merge": "1.x.x" } }, - "passport-openidconnect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.1.tgz", - "integrity": "sha512-r0QJiWEzwCg2MeCIXVP5G6YxVRqnEsZ2HpgKRthZ9AiQHJrgGUytXpsdcGF9BRwd3yMrEesb/uG/Yxb86rrY0g==", - "requires": { - "oauth": "0.9.x", - "passport-strategy": "1.x.x" - } - }, "passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", diff --git a/package.json b/package.json index a3b4a7c5..d4e9c209 100644 --- a/package.json +++ b/package.json @@ -39,10 +39,10 @@ "htmlparser2": "^8.0.1", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", + "openid-client": "^5.6.1", "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", - "passport-openidconnect": "^0.1.1", "sequelize": "^6.32.1", "socket.io": "^4.5.4", "sqlite3": "^5.1.6", diff --git a/server/Auth.js b/server/Auth.js index b7ea59c4..fa5020a0 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -5,8 +5,9 @@ const LocalStrategy = require('./libs/passportLocal') const JwtStrategy = require('passport-jwt').Strategy const ExtractJwt = require('passport-jwt').ExtractJwt const GoogleStrategy = require('passport-google-oauth20').Strategy -const OpenIDConnectStrategy = require('passport-openidconnect') +const OpenIDClient = require('openid-client') const Database = require('./Database') +const Logger = require('./Logger') /** * @class Class for handling all the authentication related functionality. @@ -62,20 +63,33 @@ class Auth { // Check if we should load the openid strategy if (global.ServerSettings.authActiveAuthMethods.includes("openid")) { - passport.use(new OpenIDConnectStrategy({ + const openIdIssuerClient = new OpenIDClient.Issuer({ issuer: global.ServerSettings.authOpenIDIssuerURL, - authorizationURL: global.ServerSettings.authOpenIDAuthorizationURL, - tokenURL: global.ServerSettings.authOpenIDTokenURL, - userInfoURL: global.ServerSettings.authOpenIDUserInfoURL, - clientID: global.ServerSettings.authOpenIDClientID, - clientSecret: global.ServerSettings.authOpenIDClientSecret, - callbackURL: '/auth/openid/callback', - scope: ["openid", "email", "profile"], - skipUserProfile: false - }, async (issuer, profile, done) => { - // TODO: do we want to create the users which does not exist? + authorization_endpoint: global.ServerSettings.authOpenIDAuthorizationURL, + token_endpoint: global.ServerSettings.authOpenIDTokenURL, + userinfo_endpoint: global.ServerSettings.authOpenIDUserInfoURL, + jwks_uri: global.ServerSettings.authOpenIDJwksURL + }).Client + const openIdClient = new openIdIssuerClient({ + client_id: global.ServerSettings.authOpenIDClientID, + client_secret: global.ServerSettings.authOpenIDClientSecret + }) + const openIdClientStrategy = new OpenIDClient.Strategy({ + client: openIdClient, + params: { + redirect_uri: '/auth/openid/callback', + scope: 'openid profile email' + } + }, async (tokenset, userinfo, done) => { + // TODO: Here is where to lookup the Abs user or register a new Abs user + Logger.debug(`[Auth] openid callback userinfo=`, userinfo) - const user = await Database.userModel.getUserByUsername(profile.username) + let user = null + // TODO: Temporary lookup existing user by email. May be replaced by a setting to toggle this or use name + if (userinfo.email && userinfo.email_verified) { + user = await Database.userModel.getUserByEmail(userinfo.email) + // TODO: If using existing user then save userinfo.sub on user + } if (!user?.isActive) { // deny login @@ -85,7 +99,12 @@ class Auth { // permit login return done(null, user) - })) + }) + // The strategy name is set to the issuer hostname by default but didnt' see a way to override this + // @see https://github.com/panva/node-openid-client/blob/a84d022f195f82ca1c97f8f6b2567ebcef8738c3/lib/passport_strategy.js#L75 + openIdClientStrategy.name = 'openid-client' + + passport.use(openIdClientStrategy) } // Load the JwtStrategy (always) -> for bearer token auth @@ -99,7 +118,7 @@ class Auth { process.nextTick(function () { // only store id to session return cb(null, JSON.stringify({ - "id": user.id, + id: user.id, })) }) }) @@ -216,7 +235,13 @@ class Auth { // openid strategy login route (this redirects to the configured openid login provider) router.get('/auth/openid', (req, res, next) => { - const auth_func = passport.authenticate('openidconnect') + // This is a (temporary?) hack to not have to get the full redirect URL from the user + // it uses the URL made in this request and adds the relative URL /auth/openid/callback + const strategy = passport._strategy('openid-client') + strategy._params.redirect_uri = new URL(`${req.protocol}://${req.get('host')}/auth/openid/callback`).toString() + + + const auth_func = passport.authenticate('openid-client') // params (isRest, callback) to a cookie that will be send to the client this.paramsToCookies(req, res) auth_func(req, res, next) @@ -224,7 +249,7 @@ class Auth { // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', - passport.authenticate('openidconnect'), + passport.authenticate('openid-client'), // on a successfull login: read the cookies and react like the client requested (callback or json) this.handleLoginSuccessBasedOnCookie.bind(this)) diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 71e2e05d..781943b4 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -68,6 +68,8 @@ class ServerSettings { this.authOpenIDAuthorizationURL = '' this.authOpenIDTokenURL = '' this.authOpenIDUserInfoURL = '' + this.authOpenIDJwksURL = '' + this.authOpenIDLogoutURL = '' this.authOpenIDClientID = '' this.authOpenIDClientSecret = '' this.authOpenIDButtonText = 'Login with OpenId' @@ -122,6 +124,8 @@ class ServerSettings { this.authOpenIDAuthorizationURL = settings.authOpenIDAuthorizationURL || '' this.authOpenIDTokenURL = settings.authOpenIDTokenURL || '' this.authOpenIDUserInfoURL = settings.authOpenIDUserInfoURL || '' + this.authOpenIDJwksURL = settings.authOpenIDJwksURL || '' + this.authOpenIDLogoutURL = settings.authOpenIDLogoutURL || '' this.authOpenIDClientID = settings.authOpenIDClientID || '' this.authOpenIDClientSecret = settings.authOpenIDClientSecret || '' this.authOpenIDButtonText = settings.authOpenIDButtonText || 'Login with OpenId' @@ -148,6 +152,7 @@ class ServerSettings { this.authOpenIDAuthorizationURL === '' || this.authOpenIDTokenURL === '' || this.authOpenIDUserInfoURL === '' || + this.authOpenIDJwksURL === '' || this.authOpenIDClientID === '' || this.authOpenIDClientSecret === '' )) { @@ -224,6 +229,8 @@ class ServerSettings { authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL, authOpenIDTokenURL: this.authOpenIDTokenURL, authOpenIDUserInfoURL: this.authOpenIDUserInfoURL, + authOpenIDJwksURL: this.authOpenIDJwksURL, + authOpenIDLogoutURL: this.authOpenIDLogoutURL, authOpenIDClientID: this.authOpenIDClientID, // Do not return to client authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client authOpenIDButtonText: this.authOpenIDButtonText, @@ -251,6 +258,8 @@ class ServerSettings { authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL, authOpenIDTokenURL: this.authOpenIDTokenURL, authOpenIDUserInfoURL: this.authOpenIDUserInfoURL, + authOpenIDJwksURL: this.authOpenIDJwksURL, + authOpenIDLogoutURL: this.authOpenIDLogoutURL, authOpenIDClientID: this.authOpenIDClientID, // Do not return to client authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client authOpenIDButtonText: this.authOpenIDButtonText, From 1e5d6a5d523c76d97c510c322186d0ae1ba849f3 Mon Sep 17 00:00:00 2001 From: Gustav Almstrom Date: Sun, 5 Nov 2023 16:51:45 +0100 Subject: [PATCH 28/39] Added swedish translation of strings --- client/strings/se.json | 729 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 729 insertions(+) create mode 100644 client/strings/se.json diff --git a/client/strings/se.json b/client/strings/se.json new file mode 100644 index 00000000..f0580847 --- /dev/null +++ b/client/strings/se.json @@ -0,0 +1,729 @@ +{ + "ButtonAdd": "Lägg till", + "ButtonAddChapters": "Lägg till kapitel", + "ButtonAddDevice": "Lägg till enhet", + "ButtonAddLibrary": "Lägg till bibliotek", + "ButtonAddPodcasts": "Lägg till podcasts", + "ButtonAddUser": "Lägg till användare", + "ButtonAddYourFirstLibrary": "Lägg till ditt första bibliotek", + "ButtonApply": "Tillämpa", + "ButtonApplyChapters": "Tillämpa kapitel", + "ButtonAuthors": "Författare", + "ButtonBrowseForFolder": "Bläddra efter mapp", + "ButtonCancel": "Avbryt", + "ButtonCancelEncode": "Avbryt kodning", + "ButtonChangeRootPassword": "Ändra rootlösenord", + "ButtonCheckAndDownloadNewEpisodes": "Kontrollera och ladda ner nya avsnitt", + "ButtonChooseAFolder": "Välj en mapp", + "ButtonChooseFiles": "Välj filer", + "ButtonClearFilter": "Rensa filter", + "ButtonCloseFeed": "Stäng flöde", + "ButtonCollections": "Samlingar", + "ButtonConfigureScanner": "Konfigurera skanner", + "ButtonCreate": "Skapa", + "ButtonCreateBackup": "Skapa säkerhetskopia", + "ButtonDelete": "Radera", + "ButtonDownloadQueue": "Kö", + "ButtonEdit": "Redigera", + "ButtonEditChapters": "Redigera kapitel", + "ButtonEditPodcast": "Redigera podcast", + "ButtonForceReScan": "Tvinga omstart", + "ButtonFullPath": "Full sökväg", + "ButtonHide": "Dölj", + "ButtonHome": "Hem", + "ButtonIssues": "Problem", + "ButtonLatest": "Senaste", + "ButtonLibrary": "Bibliotek", + "ButtonLogout": "Logga ut", + "ButtonLookup": "Sök", + "ButtonManageTracks": "Hantera spår", + "ButtonMapChapterTitles": "Karta kapitelrubriker", + "ButtonMatchAllAuthors": "Matcha alla författare", + "ButtonMatchBooks": "Matcha böcker", + "ButtonNevermind": "Glöm det", + "ButtonOk": "Okej", + "ButtonOpenFeed": "Öppna flöde", + "ButtonOpenManager": "Öppna Manager", + "ButtonPlay": "Spela", + "ButtonPlaying": "Spelar", + "ButtonPlaylists": "Spellistor", + "ButtonPurgeAllCache": "Rensa all cache", + "ButtonPurgeItemsCache": "Rensa föremåls-cache", + "ButtonPurgeMediaProgress": "Rensa medieförlopp", + "ButtonQueueAddItem": "Lägg till i kön", + "ButtonQueueRemoveItem": "Ta bort från kön", + "ButtonQuickMatch": "Snabb matchning", + "ButtonRead": "Läs", + "ButtonRemove": "Ta bort", + "ButtonRemoveAll": "Ta bort alla", + "ButtonRemoveAllLibraryItems": "Ta bort alla biblioteksobjekt", + "ButtonRemoveFromContinueListening": "Ta bort från Fortsätt lyssna", + "ButtonRemoveFromContinueReading": "Ta bort från Fortsätt läsa", + "ButtonRemoveSeriesFromContinueSeries": "Ta bort serie från Fortsätt serie", + "ButtonReScan": "Omstart", + "ButtonReset": "Återställ", + "ButtonResetToDefault": "Återställ till standard", + "ButtonRestore": "Återställ", + "ButtonSave": "Spara", + "ButtonSaveAndClose": "Spara och stäng", + "ButtonSaveTracklist": "Spara spårlista", + "ButtonScan": "Skanna", + "ButtonScanLibrary": "Skanna bibliotek", + "ButtonSearch": "Sök", + "ButtonSelectFolderPath": "Välj mappens sökväg", + "ButtonSeries": "Serie", + "ButtonSetChaptersFromTracks": "Ställ in kapitel från spår", + "ButtonShiftTimes": "Förskjut tider", + "ButtonShow": "Visa", + "ButtonStartM4BEncode": "Starta M4B-kodning", + "ButtonStartMetadataEmbed": "Starta inbäddning av metadata", + "ButtonSubmit": "Skicka", + "ButtonTest": "Testa", + "ButtonUpload": "Ladda upp", + "ButtonUploadBackup": "Ladda upp säkerhetskopia", + "ButtonUploadCover": "Ladda upp omslag", + "ButtonUploadOPMLFile": "Ladda upp OPML-fil", + "ButtonUserDelete": "Radera användare {0}", + "ButtonUserEdit": "Redigera användare {0}", + "ButtonViewAll": "Visa alla", + "ButtonYes": "Ja", + "HeaderAccount": "Konto", + "HeaderAdvanced": "Avancerad", + "HeaderAppriseNotificationSettings": "Apprise Meddelandeinställningar", + "HeaderAudiobookTools": "Ljudbokshantering", + "HeaderAudioTracks": "Ljudspår", + "HeaderBackups": "Säkerhetskopior", + "HeaderChangePassword": "Ändra lösenord", + "HeaderChapters": "Kapitel", + "HeaderChooseAFolder": "Välj en mapp", + "HeaderCollection": "Samling", + "HeaderCollectionItems": "Samlingselement", + "HeaderCover": "Omslag", + "HeaderCurrentDownloads": "Aktuella nedladdningar", + "HeaderDetails": "Detaljer", + "HeaderDownloadQueue": "Nedladdningskö", + "HeaderEbookFiles": "E-boksfiler", + "HeaderEmail": "E-post", + "HeaderEmailSettings": "E-postinställningar", + "HeaderEpisodes": "Avsnitt", + "HeaderEreaderDevices": "E-boksläsarenheter", + "HeaderEreaderSettings": "E-boksinställningar", + "HeaderFiles": "Filer", + "HeaderFindChapters": "Hitta kapitel", + "HeaderIgnoredFiles": "Ignorerade filer", + "HeaderItemFiles": "Föremålsfiler", + "HeaderItemMetadataUtils": "Metadataverktyg för föremål", + "HeaderLastListeningSession": "Senaste lyssningssession", + "HeaderLatestEpisodes": "Senaste avsnitt", + "HeaderLibraries": "Bibliotek", + "HeaderLibraryFiles": "Biblioteksfiler", + "HeaderLibraryStats": "Biblioteksstatistik", + "HeaderListeningSessions": "Lyssningssessioner", + "HeaderListeningStats": "Lyssningsstatistik", + "HeaderLogin": "Logga in", + "HeaderLogs": "Loggar", + "HeaderManageGenres": "Hantera genrer", + "HeaderManageTags": "Hantera taggar", + "HeaderMapDetails": "Karta detaljer", + "HeaderMatch": "Matcha", + "HeaderMetadataOrderOfPrecedence": "Metadataordning av företräde", + "HeaderMetadataToEmbed": "Metadata att bädda in", + "HeaderNewAccount": "Nytt konto", + "HeaderNewLibrary": "Nytt bibliotek", + "HeaderNotifications": "Meddelanden", + "HeaderOpenRSSFeed": "Öppna RSS-flöde", + "HeaderOtherFiles": "Andra filer", + "HeaderPermissions": "Behörigheter", + "HeaderPlayerQueue": "Spelarkö", + "HeaderPlaylist": "Spellista", + "HeaderPlaylistItems": "Spellistobjekt", + "HeaderPodcastsToAdd": "Podcaster att lägga till", + "HeaderPreviewCover": "Förhandsgranska omslag", + "HeaderRemoveEpisode": "Ta bort avsnitt", + "HeaderRemoveEpisodes": "Ta bort {0} avsnitt", + "HeaderRSSFeedGeneral": "RSS-information", + "HeaderRSSFeedIsOpen": "RSS-flödet är öppet", + "HeaderRSSFeeds": "RSS-flöden", + "HeaderSavedMediaProgress": "Sparad medieförlopp", + "HeaderSchedule": "Schema", + "HeaderScheduleLibraryScans": "Schemalagda biblioteksskanningar", + "HeaderSession": "Session", + "HeaderSetBackupSchedule": "Ange schemaläggning för säkerhetskopia", + "HeaderSettings": "Inställningar", + "HeaderSettingsDisplay": "Visning", + "HeaderSettingsExperimental": "Experimentella funktioner", + "HeaderSettingsGeneral": "Allmänt", + "HeaderSettingsScanner": "Skanner", + "HeaderSleepTimer": "Sovtidtagare", + "HeaderStatsLargestItems": "Största föremål", + "HeaderStatsLongestItems": "Längsta föremål (tim)", + "HeaderStatsMinutesListeningChart": "Minuters lyssning (senaste 7 dagar)", + "HeaderStatsRecentSessions": "Senaste sessioner", + "HeaderStatsTop10Authors": "Topp 10 författare", + "HeaderStatsTop5Genres": "Topp 5 genrer", + "HeaderTableOfContents": "Innehållsförteckning", + "HeaderTools": "Verktyg", + "HeaderUpdateAccount": "Uppdatera konto", + "HeaderUpdateAuthor": "Uppdatera författare", + "HeaderUpdateDetails": "Uppdatera detaljer", + "HeaderUpdateLibrary": "Uppdatera bibliotek", + "HeaderUsers": "Användare", + "HeaderYourStats": "Dina statistik", + "LabelAbridged": "Förkortad", + "LabelAccountType": "Kontotyp", + "LabelAccountTypeAdmin": "Admin", + "LabelAccountTypeGuest": "Gäst", + "LabelAccountTypeUser": "Användare", + "LabelActivity": "Aktivitet", + "LabelAdded": "Tillagd", + "LabelAddedAt": "Tillagd vid", + "LabelAddToCollection": "Lägg till i Samling", + "LabelAddToCollectionBatch": "Lägg till {0} böcker i Samlingen", + "LabelAddToPlaylist": "Lägg till i Spellista", + "LabelAddToPlaylistBatch": "Lägg till {0} objekt i Spellistan", + "LabelAdminUsersOnly": "Endast administratörer", + "LabelAll": "Alla", + "LabelAllUsers": "Alla användare", + "LabelAllUsersExcludingGuests": "Alla användare utom gäster", + "LabelAllUsersIncludingGuests": "Alla användare inklusive gäster", + "LabelAlreadyInYourLibrary": "Redan i din samling", + "LabelAppend": "Lägg till", + "LabelAuthor": "Författare", + "LabelAuthorFirstLast": "Författare (Förnamn Efternamn)", + "LabelAuthorLastFirst": "Författare (Efternamn, Förnamn)", + "LabelAuthors": "Författare", + "LabelAutoDownloadEpisodes": "Automatisk nedladdning av avsnitt", + "LabelBackToUser": "Tillbaka till användaren", + "LabelBackupLocation": "Säkerhetskopia Plats", + "LabelBackupsEnableAutomaticBackups": "Aktivera automatiska säkerhetskopior", + "LabelBackupsEnableAutomaticBackupsHelp": "Säkerhetskopior sparas i /metadata/säkerhetskopior", + "LabelBackupsMaxBackupSize": "Maximal säkerhetskopiostorlek (i GB)", + "LabelBackupsMaxBackupSizeHelp": "Som ett skydd mot felkonfiguration kommer säkerhetskopior att misslyckas om de överskrider den konfigurerade storleken.", + "LabelBackupsNumberToKeep": "Antal säkerhetskopior att behålla", + "LabelBackupsNumberToKeepHelp": "Endast en säkerhetskopia tas bort åt gången, så om du redan har fler säkerhetskopior än detta bör du ta bort dem manuellt.", + "LabelBitrate": "Bitfrekvens", + "LabelBooks": "Böcker", + "LabelChangePassword": "Ändra lösenord", + "LabelChannels": "Kanaler", + "LabelChapters": "Kapitel", + "LabelChaptersFound": "hittade kapitel", + "LabelChapterTitle": "Kapitelrubrik", + "LabelClickForMoreInfo": "Klicka för mer information", + "LabelClosePlayer": "Stäng spelaren", + "LabelCodec": "Codec", + "LabelCollapseSeries": "Fäll ihop serie", + "LabelCollection": "Samling", + "LabelCollections": "Samlingar", + "LabelComplete": "Komplett", + "LabelConfirmPassword": "Bekräfta lösenord", + "LabelContinueListening": "Fortsätt lyssna", + "LabelContinueReading": "Fortsätt läsa", + "LabelContinueSeries": "Fortsätt serie", + "LabelCover": "Omslag", + "LabelCoverImageURL": "URL till omslagsbild", + "LabelCreatedAt": "Skapad vid", + "LabelCronExpression": "Cron-uttryck", + "LabelCurrent": "Nuvarande", + "LabelCurrently": "För närvarande:", + "LabelCustomCronExpression": "Anpassat Cron-uttryck:", + "LabelDatetime": "Datum och tid", + "LabelDeleteFromFileSystemCheckbox": "Ta bort från filsystem (avmarkera för att endast ta bort från databasen)", + "LabelDescription": "Beskrivning", + "LabelDeselectAll": "Avmarkera alla", + "LabelDevice": "Enhet", + "LabelDeviceInfo": "Enhetsinformation", + "LabelDeviceIsAvailableTo": "Enhet är tillgänglig för...", + "LabelDirectory": "Katalog", + "LabelDiscFromFilename": "Skiva från filnamn", + "LabelDiscFromMetadata": "Skiva från metadata", + "LabelDiscover": "Upptäck", + "LabelDownload": "Ladda ner", + "LabelDownloadNEpisodes": "Ladda ner {0} avsnitt", + "LabelDuration": "Varaktighet", + "LabelDurationFound": "Varaktighet hittad:", + "LabelEbook": "E-bok", + "LabelEbooks": "E-böcker", + "LabelEdit": "Redigera", + "LabelEmail": "E-post", + "LabelEmailSettingsFromAddress": "Från adress", + "LabelEmailSettingsSecure": "Säker", + "LabelEmailSettingsSecureHelp": "Om sant kommer anslutningen att använda TLS vid anslutning till servern. Om falskt används TLS om servern stöder STARTTLS-tillägget. I de flesta fall, om du ansluter till port 465, bör du ställa in detta värde till sant. För port 587 eller 25, låt det vara falskt. (från nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsTestAddress": "Testadress", + "LabelEmbeddedCover": "Inbäddat omslag", + "LabelEnable": "Aktivera", + "LabelEnd": "Slut", + "LabelEpisode": "Avsnitt", + "LabelEpisodeTitle": "Avsnittsrubrik", + "LabelEpisodeType": "Avsnittstyp", + "LabelExample": "Exempel", + "LabelExplicit": "Explicit", + "LabelFeedURL": "Flödes-URL", + "LabelFile": "Fil", + "LabelFileBirthtime": "Födelse-tidpunkt för fil", + "LabelFileModified": "Fil ändrad", + "LabelFilename": "Filnamn", + "LabelFilterByUser": "Filtrera efter användare", + "LabelFindEpisodes": "Hitta avsnitt", + "LabelFinished": "Avslutad", + "LabelFolder": "Mapp", + "LabelFolders": "Mappar", + "LabelFontFamily": "Teckensnittsfamilj", + "LabelFontScale": "Teckensnittsskala", + "LabelFormat": "Format", + "LabelGenre": "Genre", + "LabelGenres": "Genrer", + "LabelHardDeleteFile": "Hård radering av fil", + "LabelHasEbook": "Har e-bok", + "LabelHasSupplementaryEbook": "Har kompletterande e-bok", + "LabelHost": "Värd", + "LabelHour": "Timme", + "LabelIcon": "Ikon", + "LabelImageURLFromTheWeb": "Bild-URL från webben", + "LabelIncludeInTracklist": "Inkludera i spårlista", + "LabelIncomplete": "Ofullständig", + "LabelInProgress": "Pågående", + "LabelInterval": "Intervall", + "LabelIntervalCustomDailyWeekly": "Anpassat dagligt/veckovis", + "LabelIntervalEvery12Hours": "Var 12:e timme", + "LabelIntervalEvery15Minutes": "Var 15:e minut", + "LabelIntervalEvery2Hours": "Var 2:e timme", + "LabelIntervalEvery30Minutes": "Var 30:e minut", + "LabelIntervalEvery6Hours": "Var 6:e timme", + "LabelIntervalEveryDay": "Varje dag", + "LabelIntervalEveryHour": "Varje timme", + "LabelInvalidParts": "Ogiltiga delar", + "LabelInvert": "Invertera", + "LabelItem": "Objekt", + "LabelLanguage": "Språk", + "LabelLanguageDefaultServer": "Standardspråk för server", + "LabelLastBookAdded": "Senaste bok tillagd", + "LabelLastBookUpdated": "Senaste bok uppdaterad", + "LabelLastSeen": "Senast sedd", + "LabelLastTime": "Senaste gången", + "LabelLastUpdate": "Senaste uppdatering", + "LabelLayout": "Layout", + "LabelLayoutSinglePage": "En sida", + "LabelLayoutSplitPage": "Dela sida", + "LabelLess": "Mindre", + "LabelLibrariesAccessibleToUser": "Åtkomliga bibliotek för användare", + "LabelLibrary": "Bibliotek", + "LabelLibraryItem": "Biblioteksobjekt", + "LabelLibraryName": "Biblioteksnamn", + "LabelLimit": "Begränsning", + "LabelLineSpacing": "Radavstånd", + "LabelListenAgain": "Lyssna igen", + "LabelLogLevelDebug": "Felsökningsnivå: Felsökning", + "LabelLogLevelInfo": "Felsökningsnivå: Information", + "LabelLogLevelWarn": "Felsökningsnivå: Varning", + "LabelLookForNewEpisodesAfterDate": "Sök efter nya avsnitt efter detta datum", + "LabelMediaPlayer": "Mediaspelare", + "LabelMediaType": "Mediatyp", + "LabelMetadataOrderOfPrecedenceDescription": "1 är lägsta prioritet, 5 är högsta prioritet", + "LabelMetadataProvider": "Metadataleverantör", + "LabelMetaTag": "Metamärke", + "LabelMetaTags": "Metamärken", + "LabelMinute": "Minut", + "LabelMissing": "Saknad", + "LabelMissingParts": "Saknade delar", + "LabelMore": "Mer", + "LabelMoreInfo": "Mer information", + "LabelName": "Namn", + "LabelNarrator": "Berättare", + "LabelNarrators": "Berättare", + "LabelNew": "Ny", + "LabelNewestAuthors": "Nyaste författare", + "LabelNewestEpisodes": "Nyaste avsnitt", + "LabelNewPassword": "Nytt lösenord", + "LabelNextBackupDate": "Nästa säkerhetskopia datum", + "LabelNextScheduledRun": "Nästa schemalagda körning", + "LabelNoEpisodesSelected": "Inga avsnitt valda", + "LabelNotes": "Anteckningar", + "LabelNotFinished": "Ej avslutad", + "LabelNotificationAppriseURL": "Apprise URL(er)", + "LabelNotificationAvailableVariables": "Tillgängliga variabler", + "LabelNotificationBodyTemplate": "Kroppsmall", + "LabelNotificationEvent": "Aviseringshändelse", + "LabelNotificationsMaxFailedAttempts": "Max antal misslyckade försök", + "LabelNotificationsMaxFailedAttemptsHelp": "Aviseringar inaktiveras när de misslyckas med att skickas så många gånger", + "LabelNotificationsMaxQueueSize": "Max köstorlek för aviseringsevenemang", + "LabelNotificationsMaxQueueSizeHelp": "Evenemang är begränsade till att utlösa ett per sekund. Evenemang kommer att ignoreras om kön är full. Detta förhindrar aviseringsspam.", + "LabelNotificationTitleTemplate": "Titelsmall", + "LabelNotStarted": "Inte påbörjad", + "LabelNumberOfBooks": "Antal böcker", + "LabelNumberOfEpisodes": "Antal avsnitt", + "LabelOpenRSSFeed": "Öppna RSS-flöde", + "LabelOverwrite": "Skriv över", + "LabelPassword": "Lösenord", + "LabelPath": "Sökväg", + "LabelPermissionsAccessAllLibraries": "Kan komma åt alla bibliotek", + "LabelPermissionsAccessAllTags": "Kan komma åt alla taggar", + "LabelPermissionsAccessExplicitContent": "Kan komma åt explicit innehåll", + "LabelPermissionsDelete": "Kan radera", + "LabelPermissionsDownload": "Kan ladda ner", + "LabelPermissionsUpdate": "Kan uppdatera", + "LabelPermissionsUpload": "Kan ladda upp", + "LabelPhotoPathURL": "Bildsökväg/URL", + "LabelPlaylists": "Spellistor", + "LabelPlayMethod": "Spelläge", + "LabelPodcast": "Podcast", + "LabelPodcasts": "Podcasts", + "LabelPodcastType": "Podcasttyp", + "LabelPort": "Port", + "LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)", + "LabelPreventIndexing": "Förhindra att ditt flöde indexeras av iTunes och Google-podcastsökmotorer", + "LabelPrimaryEbook": "Primär e-bok", + "LabelProgress": "Framsteg", + "LabelProvider": "Leverantör", + "LabelPubDate": "Publiceringsdatum", + "LabelPublisher": "Utgivare", + "LabelPublishYear": "Publiceringsår", + "LabelRead": "Läst", + "LabelReadAgain": "Läs igen", + "LabelReadEbookWithoutProgress": "Läs e-bok utan att behålla framsteg", + "LabelRecentlyAdded": "Nyligen tillagd", + "LabelRecentSeries": "Senaste serier", + "LabelRecommended": "Rekommenderad", + "LabelRegion": "Region", + "LabelReleaseDate": "Utgivningsdatum", + "LabelRemoveCover": "Ta bort omslag", + "LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post", + "LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn", + "LabelRSSFeedOpen": "Öppna RSS-flöde", + "LabelRSSFeedPreventIndexing": "Förhindra indexering", + "LabelRSSFeedSlug": "RSS-flödesslag", + "LabelRSSFeedURL": "RSS-flöde URL", + "LabelSearchTerm": "Sökterm", + "LabelSearchTitle": "Sök titel", + "LabelSearchTitleOrASIN": "Sök titel eller ASIN", + "LabelSeason": "Säsong", + "LabelSelectAllEpisodes": "Välj alla avsnitt", + "LabelSelectEpisodesShowing": "Välj {0} avsnitt som visas", + "LabelSelectUsers": "Välj användare", + "LabelSendEbookToDevice": "Skicka e-bok till...", + "LabelSequence": "Sekvens", + "LabelSeries": "Serie", + "LabelSeriesName": "Serienamn", + "LabelSeriesProgress": "Serieframsteg", + "LabelSetEbookAsPrimary": "Ange som primär", + "LabelSetEbookAsSupplementary": "Ange som kompletterande", + "LabelSettingsAudiobooksOnly": "Endast ljudböcker", + "LabelSettingsAudiobooksOnlyHelp": "Aktivera detta alternativ kommer att ignorera e-boksfiler om de inte finns inom en ljudboksmapp, i vilket fall de kommer att anges som kompletterande e-böcker", + "LabelSettingsBookshelfViewHelp": "Skeumorfisk design med trähyllor", + "LabelSettingsChromecastSupport": "Chromecast-stöd", + "LabelSettingsDateFormat": "Datumformat", + "LabelSettingsDisableWatcher": "Inaktivera Watcher", + "LabelSettingsDisableWatcherForLibrary": "Inaktivera mappbevakning för bibliotek", + "LabelSettingsDisableWatcherHelp": "Inaktiverar automatiskt lägga till/uppdatera objekt när filändringar upptäcks. *Kräver omstart av servern", + "LabelSettingsEnableWatcher": "Aktivera Watcher", + "LabelSettingsEnableWatcherForLibrary": "Aktivera mappbevakning för bibliotek", + "LabelSettingsEnableWatcherHelp": "Aktiverar automatiskt lägga till/uppdatera objekt när filändringar upptäcks. *Kräver omstart av servern", + "LabelSettingsExperimentalFeatures": "Experimentella funktioner", + "LabelSettingsExperimentalFeaturesHelp": "Funktioner under utveckling som behöver din feedback och hjälp med testning. Klicka för att öppna diskussionen på GitHub.", + "LabelSettingsFindCovers": "Hitta omslag", + "LabelSettingsFindCoversHelp": "Om din ljudbok inte har ett inbäddat omslag eller en omslagsbild i mappen kommer skannern att försöka hitta ett omslag.
Observera: Detta kommer att förlänga skannningstiden", + "LabelSettingsHideSingleBookSeries": "Dölj enboksserier", + "LabelSettingsHideSingleBookSeriesHelp": "Serier som har en enda bok kommer att döljas från seriesidan och hyllsidan på startsidan.", + "LabelSettingsHomePageBookshelfView": "Startsida använd bokhyllvy", + "LabelSettingsLibraryBookshelfView": "Bibliotek använd bokhyllvy", + "LabelSettingsParseSubtitles": "Analysera undertexter", + "LabelSettingsParseSubtitlesHelp": "Extrahera undertexter från mappnamn för ljudböcker.
Undertext måste vara åtskilda av \" - \"
t.ex. \"Boktitel - En undertitel här\" har undertiteln \"En undertitel här\"", + "LabelSettingsPreferMatchedMetadata": "Föredra matchad metadata", + "LabelSettingsPreferMatchedMetadataHelp": "Matchad data kommer att åsidosätta objektdetaljer vid snabbmatchning. Som standard kommer snabbmatchning endast att fylla i saknade detaljer.", + "LabelSettingsSkipMatchingBooksWithASIN": "Hoppa över matchande böcker med ASIN", + "LabelSettingsSkipMatchingBooksWithISBN": "Hoppa över matchande böcker med ISBN", + "LabelSettingsSortingIgnorePrefixes": "Ignorera prefix vid sortering", + "LabelSettingsSortingIgnorePrefixesHelp": "t.ex. för prefixet \"the\" kommer boktiteln \"The Book Title\" att sorteras som \"Book Title, The\"", + "LabelSettingsSquareBookCovers": "Använd fyrkantiga bokomslag", + "LabelSettingsSquareBookCoversHelp": "Föredrar att använda fyrkantiga omslag över standard 1.6:1 bokomslag", + "LabelSettingsStoreCoversWithItem": "Lagra omslag med objekt", + "LabelSettingsStoreCoversWithItemHelp": "Som standard lagras omslag i /metadata/items, att aktivera detta alternativ kommer att lagra omslag i din biblioteksmapp. Endast en fil med namnet \"cover\" kommer att behållas", + "LabelSettingsStoreMetadataWithItem": "Lagra metadata med objekt", + "LabelSettingsStoreMetadataWithItemHelp": "Som standard lagras metadatafiler i /metadata/items, att aktivera detta alternativ kommer att lagra metadatafiler i dina biblioteksmappar", + "LabelSettingsTimeFormat": "Tidsformat", + "LabelShowAll": "Visa alla", + "LabelSize": "Storlek", + "LabelSleepTimer": "Sleeptimer", + "LabelSlug": "Slug", + "LabelStart": "Start", + "LabelStarted": "Startad", + "LabelStartedAt": "Startad vid", + "LabelStartTime": "Starttid", + "LabelStatsAudioTracks": "Ljudspår", + "LabelStatsAuthors": "Författare", + "LabelStatsBestDay": "Bästa dag", + "LabelStatsDailyAverage": "Dagligt genomsnitt", + "LabelStatsDays": "Dagar", + "LabelStatsDaysListened": "Dagar lyssnade", + "LabelStatsHours": "Timmar", + "LabelStatsInARow": "i rad", + "LabelStatsItemsFinished": "Objekt avslutade", + "LabelStatsItemsInLibrary": "Objekt i biblioteket", + "LabelStatsMinutes": "minuter", + "LabelStatsMinutesListening": "Minuter av lyssnande", + "LabelStatsOverallDays": "Totalt antal dagar", + "LabelStatsOverallHours": "Totalt antal timmar", + "LabelStatsWeekListening": "Veckans lyssnande", + "LabelSubtitle": "Underrubrik", + "LabelSupportedFileTypes": "Stödda filtyper", + "LabelTag": "Tagg", + "LabelTags": "Taggar", + "LabelTagsAccessibleToUser": "Taggar tillgängliga för användaren", + "LabelTagsNotAccessibleToUser": "Taggar inte tillgängliga för användaren", + "LabelTasks": "Körande uppgifter", + "LabelTheme": "Tema", + "LabelThemeDark": "Mörkt", + "LabelThemeLight": "Ljust", + "LabelTimeBase": "Tidsbas", + "LabelTimeListened": "Tid lyssnad", + "LabelTimeListenedToday": "Tid lyssnad idag", + "LabelTimeRemaining": "{0} kvar", + "LabelTimeToShift": "Tid att skifta i sekunder", + "LabelTitle": "Titel", + "LabelToolsEmbedMetadata": "Bädda in metadata", + "LabelToolsEmbedMetadataDescription": "Bädda in metadata i ljudfiler, inklusive omslagsbild och kapitel.", + "LabelToolsMakeM4b": "Skapa M4B ljudbok", + "LabelToolsMakeM4bDescription": "Skapa en .M4B ljudboksfil med inbäddad metadata, omslagsbild och kapitel.", + "LabelToolsSplitM4b": "Dela M4B till MP3-filer", + "LabelToolsSplitM4bDescription": "Skapa MP3-filer från en M4B fil uppdelad i kapitel med inbäddad metadata, omslagsbild och kapitel.", + "LabelTotalDuration": "Total varaktighet", + "LabelTotalTimeListened": "Total tid lyssnad", + "LabelTrackFromFilename": "Spår från filnamn", + "LabelTrackFromMetadata": "Spår från metadata", + "LabelTracks": "Spår", + "LabelTracksMultiTrack": "Flerspårigt", + "LabelTracksNone": "Inga spår", + "LabelTracksSingleTrack": "Enspårigt", + "LabelType": "Typ", + "LabelUnabridged": "Oavkortad", + "LabelUnknown": "Okänd", + "LabelUpdateCover": "Uppdatera omslag", + "LabelUpdateCoverHelp": "Tillåt överskrivning av befintliga omslag för de valda böckerna när en matchning hittas", + "LabelUpdatedAt": "Uppdaterad vid", + "LabelUpdateDetails": "Uppdatera detaljer", + "LabelUpdateDetailsHelp": "Tillåt överskrivning av befintliga detaljer för de valda böckerna när en matchning hittas", + "LabelUploaderDragAndDrop": "Dra och släpp filer eller mappar", + "LabelUploaderDropFiles": "Släpp filer", + "LabelUseChapterTrack": "Använd kapitelspår", + "LabelUseFullTrack": "Använd hela spåret", + "LabelUser": "Användare", + "LabelUsername": "Användarnamn", + "LabelValue": "Värde", + "LabelVersion": "Version", + "LabelViewBookmarks": "Visa bokmärken", + "LabelViewChapters": "Visa kapitel", + "LabelViewQueue": "Visa spellista", + "LabelVolume": "Volym", + "LabelWeekdaysToRun": "Vardagar att köra", + "LabelYourAudiobookDuration": "Din ljudboks varaktighet", + "LabelYourBookmarks": "Dina bokmärken", + "LabelYourPlaylists": "Dina spellistor", + "LabelYourProgress": "Din framsteg", + "MessageAddToPlayerQueue": "Lägg till i spellistan", + "MessageAppriseDescription": "För att använda den här funktionen behöver du ha en instans av Apprise API igång eller en API som hanterar dessa begäranden.
Apprise API-urlen bör vara hela URL-sökvägen för att skicka meddelandet, t.ex., om din API-instans är tillgänglig på http://192.168.1.1:8337, bör du ange http://192.168.1.1:8337/notify.", + "MessageBackupsDescription": "Säkerhetskopieringar inkluderar användare, användares framsteg, biblioteksföremål, serverinställningar och bilder lagrade i /metadata/items & /metadata/authors. Säkerhetskopieringar inkluderar inte några filer lagrade i dina biblioteksmappar.", + "MessageBatchQuickMatchDescription": "Quick Match kommer försöka lägga till saknade omslag och metadata för de valda föremålen. Aktivera alternativen nedan för att tillåta Quick Match att överskriva befintliga omslag och/eller metadata.", + "MessageBookshelfNoCollections": "Du har ännu inte skapat några samlingar", + "MessageBookshelfNoResultsForFilter": "Inga resultat för filter \"{0}: {1}\"", + "MessageBookshelfNoRSSFeeds": "Inga RSS-flöden är öppna", + "MessageBookshelfNoSeries": "Du har inga serier", + "MessageChapterEndIsAfter": "Kapitelns slut är efter din ljudboks slut", + "MessageChapterErrorFirstNotZero": "Första kapitlet måste börja vid 0", + "MessageChapterErrorStartGteDuration": "Ogiltig starttid måste vara mindre än ljudbokens varaktighet", + "MessageChapterErrorStartLtPrev": "Ogiltig starttid måste vara större än eller lika med tidigare kapitels starttid", + "MessageChapterStartIsAfter": "Kapitlets start är efter din ljudboks slut", + "MessageCheckingCron": "Kontrollerar cron...", + "MessageConfirmCloseFeed": "Är du säker på att du vill stänga detta flöde?", + "MessageConfirmDeleteBackup": "Är du säker på att du vill radera säkerhetskopian för {0}?", + "MessageConfirmDeleteFile": "Detta kommer att radera filen från ditt filsystem. Är du säker?", + "MessageConfirmDeleteLibrary": "Är du säker på att du vill radera biblioteket \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "Detta kommer att radera biblioteksföremålet från databasen och ditt filsystem. Är du säker?", + "MessageConfirmDeleteLibraryItems": "Detta kommer att radera {0} biblioteksföremål från databasen och ditt filsystem. Är du säker?", + "MessageConfirmDeleteSession": "Är du säker på att du vill radera denna session?", + "MessageConfirmForceReScan": "Är du säker på att du vill tvinga omgenomsökning?", + "MessageConfirmMarkAllEpisodesFinished": "Är du säker på att du vill markera alla avsnitt som avslutade?", + "MessageConfirmMarkAllEpisodesNotFinished": "Är du säker på att du vill markera alla avsnitt som inte avslutade?", + "MessageConfirmMarkSeriesFinished": "Är du säker på att du vill markera alla böcker i denna serie som avslutade?", + "MessageConfirmMarkSeriesNotFinished": "Är du säker på att du vill markera alla böcker i denna serie som inte avslutade?", + "MessageConfirmQuickEmbed": "Varning! Quick embed kommer inte att säkerhetskopiera dina ljudfiler. Se till att du har en säkerhetskopia av dina ljudfiler.

Vill du fortsätta?", + "MessageConfirmRemoveAllChapters": "Är du säker på att du vill ta bort alla kapitel?", + "MessageConfirmRemoveAuthor": "Är du säker på att du vill ta bort författaren \"{0}\"?", + "MessageConfirmRemoveCollection": "Är du säker på att du vill ta bort samlingen \"{0}\"?", + "MessageConfirmRemoveEpisode": "Är du säker på att du vill ta bort avsnittet \"{0}\"?", + "MessageConfirmRemoveEpisodes": "Är du säker på att du vill ta bort {0} avsnitt?", + "MessageConfirmRemoveNarrator": "Är du säker på att du vill ta bort berättaren \"{0}\"?", + "MessageConfirmRemovePlaylist": "Är du säker på att du vill ta bort din spellista \"{0}\"?", + "MessageConfirmRenameGenre": "Är du säker på att du vill byta namn på genren \"{0}\" till \"{1}\" för alla objekt?", + "MessageConfirmRenameGenreMergeNote": "Observera: Den här genren finns redan, så de kommer att slås samman.", + "MessageConfirmRenameGenreWarning": "Varning! En liknande genre med annat skrivsätt finns redan \"{0}\".", + "MessageConfirmRenameTag": "Är du säker på att du vill byta namn på taggen \"{0}\" till \"{1}\" för alla objekt?", + "MessageConfirmRenameTagMergeNote": "Observera: Den här taggen finns redan, så de kommer att slås samman.", + "MessageConfirmRenameTagWarning": "Varning! En liknande tagg med annat skrivsätt finns redan \"{0}\".", + "MessageConfirmReScanLibraryItems": "Är du säker på att du vill göra omgenomsökning för {0} objekt?", + "MessageConfirmSendEbookToDevice": "Är du säker på att du vill skicka {0} e-bok \"{1}\" till enheten \"{2}\"?", + "MessageDownloadingEpisode": "Laddar ner avsnitt", + "MessageDragFilesIntoTrackOrder": "Dra filer till rätt spårordning", + "MessageEmbedFinished": "Inbäddning klar!", + "MessageEpisodesQueuedForDownload": "{0} avsnitt i kö för nedladdning", + "MessageFeedURLWillBe": "Flödes-URL kommer att vara {0}", + "MessageFetching": "Hämtar...", + "MessageForceReScanDescription": "kommer att göra en omgångssökning av alla filer som en färsk sökning. ID3-taggar för ljudfiler, OPF-filer och textfiler kommer att sökas som nya.", + "MessageImportantNotice": "Viktig meddelande!", + "MessageInsertChapterBelow": "Infoga kapitel nedanför", + "MessageItemsSelected": "{0} Objekt markerade", + "MessageItemsUpdated": "{0} Objekt uppdaterade", + "MessageJoinUsOn": "Anslut dig till oss på", + "MessageListeningSessionsInTheLastYear": "{0} lyssningssessioner det senaste året", + "MessageLoading": "Laddar...", + "MessageLoadingFolders": "Laddar mappar...", + "MessageM4BFailed": "M4B misslyckades!", + "MessageM4BFinished": "M4B klar!", + "MessageMapChapterTitles": "Kartlägg kapitelrubriker till dina befintliga ljudbokskapitel utan att justera tidstämplar", + "MessageMarkAllEpisodesFinished": "Markera alla avsnitt som avslutade", + "MessageMarkAllEpisodesNotFinished": "Markera alla avsnitt som inte avslutade", + "MessageMarkAsFinished": "Markera som avslutad", + "MessageMarkAsNotFinished": "Markera som inte avslutad", + "MessageMatchBooksDescription": "kommer att försöka matcha böcker i biblioteket med en bok från den valda sökleverantören och fylla i tomma detaljer och omslagskonst. Överskriver inte detaljer.", + "MessageNoAudioTracks": "Inga ljudspår", + "MessageNoAuthors": "Inga författare", + "MessageNoBackups": "Inga säkerhetskopior", + "MessageNoBookmarks": "Inga bokmärken", + "MessageNoChapters": "Inga kapitel", + "MessageNoCollections": "Inga samlingar", + "MessageNoCoversFound": "Inga omslag hittade", + "MessageNoDescription": "Ingen beskrivning", + "MessageNoDownloadsInProgress": "Inga nedladdningar pågår för närvarande", + "MessageNoDownloadsQueued": "Inga nedladdningar i kö", + "MessageNoEpisodeMatchesFound": "Inga matchande avsnitt hittades", + "MessageNoEpisodes": "Inga avsnitt", + "MessageNoFoldersAvailable": "Inga mappar tillgängliga", + "MessageNoGenres": "Inga genrer", + "MessageNoIssues": "Inga problem", + "MessageNoItems": "Inga objekt", + "MessageNoItemsFound": "Inga objekt hittades", + "MessageNoListeningSessions": "Inga lyssningssessioner", + "MessageNoLogs": "Inga loggar", + "MessageNoMediaProgress": "Ingen medieförlopp", + "MessageNoNotifications": "Inga aviseringar", + "MessageNoPodcastsFound": "Inga podcasts hittade", + "MessageNoResults": "Inga resultat", + "MessageNoSearchResultsFor": "Inga sökresultat för \"{0}\"", + "MessageNoSeries": "Inga serier", + "MessageNoTags": "Inga taggar", + "MessageNoTasksRunning": "Inga pågående uppgifter", + "MessageNotYetImplemented": "Ännu inte implementerad", + "MessageNoUpdateNecessary": "Ingen uppdatering krävs", + "MessageNoUpdatesWereNecessary": "Inga uppdateringar var nödvändiga", + "MessageNoUserPlaylists": "Du har inga spellistor", + "MessageOr": "eller", + "MessagePauseChapter": "Pausa kapiteluppspelning", + "MessagePlayChapter": "Lyssna på kapitlets början", + "MessagePlaylistCreateFromCollection": "Skapa spellista från samling", + "MessagePodcastHasNoRSSFeedForMatching": "Podcasten har ingen RSS-flödes-URL att använda för matchning", + "MessageQuickMatchDescription": "Fyll tomma objektdetaljer och omslag med första matchningsresultat från '{0}'. Överskriver inte detaljer om inte serverinställningen 'Föredra matchad metadata' är aktiverad.", + "MessageRemoveChapter": "Ta bort kapitel", + "MessageRemoveEpisodes": "Ta bort {0} avsnitt", + "MessageRemoveFromPlayerQueue": "Ta bort från spellistan", + "MessageRemoveUserWarning": "Är du säker på att du vill radera användaren \"{0}\" permanent?", + "MessageReportBugsAndContribute": "Rapportera buggar, begär funktioner och bidra på", + "MessageResetChaptersConfirm": "Är du säker på att du vill återställa kapitel och ångra ändringarna du gjort?", + "MessageRestoreBackupConfirm": "Är du säker på att du vill återställa säkerhetskopian som skapades den", + "MessageRestoreBackupWarning": "Att återställa en säkerhetskopia kommer att skriva över hela databasen som finns i /config och omslagsbilder i /metadata/items & /metadata/authors.

Säkerhetskopior ändrar inte några filer i dina biblioteksmappar. Om du har aktiverat serverinställningar för att lagra omslagskonst och metadata i dina biblioteksmappar säkerhetskopieras eller skrivs de inte över.

Alla klienter som använder din server kommer att uppdateras automatiskt.", + "MessageSearchResultsFor": "Sökresultat för", + "MessageServerCouldNotBeReached": "Servern kunde inte nås", + "MessageSetChaptersFromTracksDescription": "Ställ in kapitel med varje ljudfil som ett kapitel och kapitelrubrik som ljudfilens namn", + "MessageStartPlaybackAtTime": "Starta uppspelning för \"{0}\" kl. {1}?", + "MessageThinking": "Tänker...", + "MessageUploaderItemFailed": "Misslyckades med att ladda upp", + "MessageUploaderItemSuccess": "Uppladdning lyckades!", + "MessageUploading": "Laddar upp...", + "MessageValidCronExpression": "Giltigt cron-uttryck", + "MessageWatcherIsDisabledGlobally": "Vakten är inaktiverad globalt i serverinställningarna", + "MessageXLibraryIsEmpty": "{0} biblioteket är tomt!", + "MessageYourAudiobookDurationIsLonger": "Varaktigheten på din ljudbok är längre än den hittade varaktigheten", + "MessageYourAudiobookDurationIsShorter": "Varaktigheten på din ljudbok är kortare än den hittade varaktigheten", + "NoteChangeRootPassword": "Rotanvändaren är den enda användaren som kan ha ett tomt lösenord", + "NoteChapterEditorTimes": "Obs: Starttiden för första kapitlet måste förbli 0:00 och starttiden för det sista kapitlet får inte överstiga ljudbokens varaktighet.", + "NoteFolderPicker": "Obs: Mappar som redan är kartlagda kommer inte att visas", + "NoteFolderPickerDebian": "Obs: Mappväljaren för Debian-installationen är inte fullständigt implementerad. Du bör ange sökvägen till ditt bibliotek direkt.", + "NoteRSSFeedPodcastAppsHttps": "Varning: De flesta podcastappar kräver att RSS-flödets URL används med HTTPS", + "NoteRSSFeedPodcastAppsPubDate": "Varning: 1 eller flera av dina avsnitt har inte ett publiceringsdatum. Vissa podcastappar kräver detta.", + "NoteUploaderFoldersWithMediaFiles": "Mappar med mediefiler hanteras som separata biblioteksobjekt.", + "NoteUploaderOnlyAudioFiles": "Om du bara laddar upp ljudfiler kommer varje ljudfil att hanteras som en separat ljudbok.", + "NoteUploaderUnsupportedFiles": "Oaccepterade filer ignoreras. När du väljer eller släpper en mapp ignoreras andra filer som inte finns i ett objektmapp.", + "PlaceholderNewCollection": "Nytt samlingsnamn", + "PlaceholderNewFolderPath": "Nytt mappväg", + "PlaceholderNewPlaylist": "Nytt spellistanamn", + "PlaceholderSearch": "Sök...", + "PlaceholderSearchEpisode": "Sök avsnitt...", + "ToastAccountUpdateFailed": "Det gick inte att uppdatera kontot", + "ToastAccountUpdateSuccess": "Kontot uppdaterat", + "ToastAuthorImageRemoveFailed": "Det gick inte att ta bort författarens bild", + "ToastAuthorImageRemoveSuccess": "Författarens bild borttagen", + "ToastAuthorUpdateFailed": "Det gick inte att uppdatera författaren", + "ToastAuthorUpdateMerged": "Författaren sammanslagen", + "ToastAuthorUpdateSuccess": "Författaren uppdaterad", + "ToastAuthorUpdateSuccessNoImageFound": "Författaren uppdaterad (ingen bild hittad)", + "ToastBackupCreateFailed": "Det gick inte att skapa en säkerhetskopia", + "ToastBackupCreateSuccess": "Säkerhetskopia skapad", + "ToastBackupDeleteFailed": "Det gick inte att ta bort säkerhetskopian", + "ToastBackupDeleteSuccess": "Säkerhetskopan borttagen", + "ToastBackupRestoreFailed": "Det gick inte att återställa säkerhetskopan", + "ToastBackupUploadFailed": "Det gick inte att ladda upp säkerhetskopan", + "ToastBackupUploadSuccess": "Säkerhetskopan uppladdad", + "ToastBatchUpdateFailed": "Batchuppdateringen misslyckades", + "ToastBatchUpdateSuccess": "Batchuppdateringen lyckades", + "ToastBookmarkCreateFailed": "Det gick inte att skapa bokmärket", + "ToastBookmarkCreateSuccess": "Bokmärket tillagt", + "ToastBookmarkRemoveFailed": "Det gick inte att ta bort bokmärket", + "ToastBookmarkRemoveSuccess": "Bokmärket borttaget", + "ToastBookmarkUpdateFailed": "Det gick inte att uppdatera bokmärket", + "ToastBookmarkUpdateSuccess": "Bokmärket uppdaterat", + "ToastChaptersHaveErrors": "Kapitlen har fel", + "ToastChaptersMustHaveTitles": "Kapitel måste ha titlar", + "ToastCollectionItemsRemoveFailed": "Det gick inte att ta bort objekt från samlingen", + "ToastCollectionItemsRemoveSuccess": "Objekt borttagna från samlingen", + "ToastCollectionRemoveFailed": "Det gick inte att ta bort samlingen", + "ToastCollectionRemoveSuccess": "Samlingen borttagen", + "ToastCollectionUpdateFailed": "Det gick inte att uppdatera samlingen", + "ToastCollectionUpdateSuccess": "Samlingen uppdaterad", + "ToastItemCoverUpdateFailed": "Det gick inte att uppdatera objektets omslag", + "ToastItemCoverUpdateSuccess": "Objektets omslag uppdaterat", + "ToastItemDetailsUpdateFailed": "Det gick inte att uppdatera objektdetaljerna", + "ToastItemDetailsUpdateSuccess": "Objektdetaljer uppdaterade", + "ToastItemDetailsUpdateUnneeded": "Inga uppdateringar behövs för objektdetaljerna", + "ToastItemMarkedAsFinishedFailed": "Misslyckades med att markera som färdig", + "ToastItemMarkedAsFinishedSuccess": "Objekt markerat som färdig", + "ToastItemMarkedAsNotFinishedFailed": "Misslyckades med att markera som ej färdig", + "ToastItemMarkedAsNotFinishedSuccess": "Objekt markerat som ej färdig", + "ToastLibraryCreateFailed": "Det gick inte att skapa biblioteket", + "ToastLibraryCreateSuccess": "Biblioteket \"{0}\" skapat", + "ToastLibraryDeleteFailed": "Det gick inte att ta bort biblioteket", + "ToastLibraryDeleteSuccess": "Biblioteket borttaget", + "ToastLibraryScanFailedToStart": "Misslyckades med att starta skanningen", + "ToastLibraryScanStarted": "Skanning av biblioteket påbörjad", + "ToastLibraryUpdateFailed": "Det gick inte att uppdatera biblioteket", + "ToastLibraryUpdateSuccess": "Biblioteket \"{0}\" uppdaterat", + "ToastPlaylistCreateFailed": "Det gick inte att skapa spellistan", + "ToastPlaylistCreateSuccess": "Spellistan skapad", + "ToastPlaylistRemoveFailed": "Det gick inte att ta bort spellistan", + "ToastPlaylistRemoveSuccess": "Spellistan borttagen", + "ToastPlaylistUpdateFailed": "Det gick inte att uppdatera spellistan", + "ToastPlaylistUpdateSuccess": "Spellistan uppdaterad", + "ToastPodcastCreateFailed": "Misslyckades med att skapa podcasten", + "ToastPodcastCreateSuccess": "Podcasten skapad framgångsrikt", + "ToastRemoveItemFromCollectionFailed": "Misslyckades med att ta bort objektet från samlingen", + "ToastRemoveItemFromCollectionSuccess": "Objektet borttaget från samlingen", + "ToastRSSFeedCloseFailed": "Misslyckades med att stänga RSS-flödet", + "ToastRSSFeedCloseSuccess": "RSS-flödet stängt", + "ToastSendEbookToDeviceFailed": "Misslyckades med att skicka e-boken till enheten", + "ToastSendEbookToDeviceSuccess": "E-boken skickad till enheten \"{0}\"", + "ToastSeriesUpdateFailed": "Serieuppdateringen misslyckades", + "ToastSeriesUpdateSuccess": "Serieuppdateringen lyckades", + "ToastSessionDeleteFailed": "Misslyckades med att ta bort sessionen", + "ToastSessionDeleteSuccess": "Sessionen borttagen", + "ToastSocketConnected": "Socket ansluten", + "ToastSocketDisconnected": "Socket frånkopplad", + "ToastSocketFailedToConnect": "Socket misslyckades med att ansluta", + "ToastUserDeleteFailed": "Misslyckades med att ta bort användaren", + "ToastUserDeleteSuccess": "Användaren borttagen" + } From 61e05e92a8e133c19b557b166597fc3771a3ef3a Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 5 Nov 2023 10:16:40 -0600 Subject: [PATCH 29/39] Add Swedish language option --- client/plugins/i18n.js | 1 + client/strings/{se.json => sv.json} | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) rename client/strings/{se.json => sv.json} (99%) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 9a7eb02e..ea6a06db 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -18,6 +18,7 @@ const languageCodeMap = { 'no': { label: 'Norsk', dateFnsLocale: 'no' }, 'pl': { label: 'Polski', dateFnsLocale: 'pl' }, 'ru': { label: 'Русский', dateFnsLocale: 'ru' }, + 'sv': { label: 'Svenska', dateFnsLocale: 'sv' }, 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }, } Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => { diff --git a/client/strings/se.json b/client/strings/sv.json similarity index 99% rename from client/strings/se.json rename to client/strings/sv.json index f0580847..23d489d0 100644 --- a/client/strings/se.json +++ b/client/strings/sv.json @@ -726,4 +726,4 @@ "ToastSocketFailedToConnect": "Socket misslyckades med att ansluta", "ToastUserDeleteFailed": "Misslyckades med att ta bort användaren", "ToastUserDeleteSuccess": "Användaren borttagen" - } +} \ No newline at end of file From 309ef807abb21e6eb25af525a9670ad1c3352a8a Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 5 Nov 2023 12:37:05 -0600 Subject: [PATCH 30/39] Update /auth/openid endpoint to work with PKCE from mobile Co-authored-by: Denis Arnst --- server/Auth.js | 82 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index fa5020a0..a04f9ac4 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -74,7 +74,7 @@ class Auth { client_id: global.ServerSettings.authOpenIDClientID, client_secret: global.ServerSettings.authOpenIDClientSecret }) - const openIdClientStrategy = new OpenIDClient.Strategy({ + passport.use('openid-client', new OpenIDClient.Strategy({ client: openIdClient, params: { redirect_uri: '/auth/openid/callback', @@ -99,12 +99,7 @@ class Auth { // permit login return done(null, user) - }) - // The strategy name is set to the issuer hostname by default but didnt' see a way to override this - // @see https://github.com/panva/node-openid-client/blob/a84d022f195f82ca1c97f8f6b2567ebcef8738c3/lib/passport_strategy.js#L75 - openIdClientStrategy.name = 'openid-client' - - passport.use(openIdClientStrategy) + })) } // Load the JwtStrategy (always) -> for bearer token auth @@ -235,16 +230,77 @@ class Auth { // openid strategy login route (this redirects to the configured openid login provider) router.get('/auth/openid', (req, res, next) => { - // This is a (temporary?) hack to not have to get the full redirect URL from the user - // it uses the URL made in this request and adds the relative URL /auth/openid/callback - const strategy = passport._strategy('openid-client') - strategy._params.redirect_uri = new URL(`${req.protocol}://${req.get('host')}/auth/openid/callback`).toString() + // helper function from openid-client + function pick(object, ...paths) { + const obj = {} + for (const path of paths) { + if (object[path] !== undefined) { + obj[path] = object[path] + } + } + return obj + } + // Get the OIDC client from the strategy + // We need to call the client manually, because the strategy does not support forwarding the code challenge + // for API or mobile clients + const oidcStrategy = passport._strategy('openid-client') + oidcStrategy._params.redirect_uri = new URL(`${req.protocol}://${req.get('host')}/auth/openid/callback`).toString() + const client = oidcStrategy._client + const sessionKey = oidcStrategy._key + + let code_challenge + let code_challenge_method + + // If code_challenge is provided, expect that code_verifier will be handled by the client (mobile app) + // The web frontend of ABS does not need to do a PKCE itself, because it never handles the "code" of the oauth flow + // and as such will not send a code challenge, we will generate then one + if (req.query.code_challenge) { + code_challenge = req.query.code_challenge + code_challenge_method = req.query.code_challenge_method || 'S256' + + if (!['S256', 'plain'].includes(code_challenge_method)) { + return res.status(400).send('Invalid code_challenge_method') + } + } else { + // If no code_challenge is provided, assume a web application flow and generate one + const code_verifier = OpenIDClient.generators.codeVerifier() + code_challenge = OpenIDClient.generators.codeChallenge(code_verifier) + code_challenge_method = 'S256' + + // Store the code_verifier in the session for later use in the token exchange + req.session[sessionKey] = { ...req.session[sessionKey], code_verifier } + } + + const params = { + state: OpenIDClient.generators.random(), + // Other params by the passport strategy + ...oidcStrategy._params + } + + if (!params.nonce && params.response_type.includes('id_token')) { + params.nonce = OpenIDClient.generators.random() + } + + req.session[sessionKey] = { + ...req.session[sessionKey], + ...pick(params, 'nonce', 'state', 'max_age', 'response_type') + } + + // Now get the URL to direct to + const authorizationUrl = client.authorizationUrl({ + ...params, + scope: 'openid profile email', + response_type: 'code', + code_challenge, + code_challenge_method, + }) - const auth_func = passport.authenticate('openid-client') // params (isRest, callback) to a cookie that will be send to the client this.paramsToCookies(req, res) - auth_func(req, res, next) + + // Redirect the user agent (browser) to the authorization URL + res.redirect(authorizationUrl) }) // openid strategy callback route (this receives the token from the configured openid login provider) From c17540e191b202a53c4ecf580f0e527343fd9eca Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 5 Nov 2023 12:43:34 -0600 Subject: [PATCH 31/39] Add app and serverVersion properties to response from /status --- server/Server.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/Server.js b/server/Server.js index 6c6a17b0..df6c9003 100644 --- a/server/Server.js +++ b/server/Server.js @@ -228,6 +228,8 @@ class Server { // status check for client to see if server has been initialized // server has been initialized if a root user exists const payload = { + app: 'audiobookshelf', + serverVersion: version, isInit: Database.hasRootUser, language: Database.serverSettings.language, authMethods: Database.serverSettings.authActiveAuthMethods, From f840aa80f8989a5d64b0a8e5935866906132a316 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 5 Nov 2023 14:11:37 -0600 Subject: [PATCH 32/39] Add button to populate openid URLs using the issuer URL --- client/pages/config/authentication.vue | 43 +++++++++++++++++++++++++- server/Auth.js | 27 ++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 13867ef3..7cedfd25 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -15,7 +15,17 @@
- +
+
+ +
+
+ + auto_fix_high + Auto-populate +
+
@@ -83,6 +93,37 @@ export default { } }, methods: { + autoPopulateOIDCClick() { + if (!this.newAuthSettings.authOpenIDIssuerURL) { + this.$toast.error('Issuer URL required') + return + } + // Remove trailing slash + let issuerUrl = this.newAuthSettings.authOpenIDIssuerURL + if (issuerUrl.endsWith('/')) issuerUrl = issuerUrl.slice(0, -1) + + // If the full config path is on the issuer url then remove it + if (issuerUrl.endsWith('/.well-known/openid-configuration')) { + issuerUrl = issuerUrl.replace('/.well-known/openid-configuration', '') + this.newAuthSettings.authOpenIDIssuerURL = this.newAuthSettings.authOpenIDIssuerURL.replace('/.well-known/openid-configuration', '') + } + + this.$axios + .$get(`/auth/openid/config?issuer=${issuerUrl}`) + .then((data) => { + if (data.issuer) this.newAuthSettings.authOpenIDIssuerURL = data.issuer + if (data.authorization_endpoint) this.newAuthSettings.authOpenIDAuthorizationURL = data.authorization_endpoint + if (data.token_endpoint) this.newAuthSettings.authOpenIDTokenURL = data.token_endpoint + if (data.userinfo_endpoint) this.newAuthSettings.authOpenIDUserInfoURL = data.userinfo_endpoint + if (data.end_session_endpoint) this.newAuthSettings.authOpenIDLogoutURL = data.end_session_endpoint + if (data.jwks_uri) this.newAuthSettings.authOpenIDJwksURL = data.jwks_uri + }) + .catch((error) => { + console.error('Failed to receive data', error) + const errorMsg = error.response?.data || 'Unknown error' + this.$toast.error(errorMsg) + }) + }, validateOpenID() { let isValid = true if (!this.newAuthSettings.authOpenIDIssuerURL) { diff --git a/server/Auth.js b/server/Auth.js index a04f9ac4..361380f8 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -1,3 +1,4 @@ +const axios = require('axios') const passport = require('passport') const bcrypt = require('./libs/bcryptjs') const jwt = require('./libs/jsonwebtoken') @@ -309,6 +310,32 @@ class Auth { // on a successfull login: read the cookies and react like the client requested (callback or json) this.handleLoginSuccessBasedOnCookie.bind(this)) + /** + * Used to auto-populate the openid URLs in config/authentication + */ + router.get('/auth/openid/config', async (req, res) => { + if (!req.query.issuer) { + return res.status(400).send('Invalid request. Query param \'issuer\' is required') + } + let issuerUrl = req.query.issuer + if (issuerUrl.endsWith('/')) issuerUrl = issuerUrl.slice(0, -1) + + const configUrl = `${issuerUrl}/.well-known/openid-configuration` + axios.get(configUrl).then(({ data }) => { + res.json({ + issuer: data.issuer, + authorization_endpoint: data.authorization_endpoint, + token_endpoint: data.token_endpoint, + userinfo_endpoint: data.userinfo_endpoint, + end_session_endpoint: data.end_session_endpoint, + jwks_uri: data.jwks_uri + }) + }).catch((error) => { + Logger.error(`[Auth] Failed to get openid configuration at "${configUrl}"`, error) + res.status(error.statusCode || 400).send(`${error.code || 'UNKNOWN'}: Failed to get openid configuration`) + }) + }) + // Logout route router.post('/logout', (req, res) => { // TODO: invalidate possible JWTs From e140897313b4cb7cbdc137f38a3c1b8901aadaf1 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 8 Nov 2023 14:45:29 -0600 Subject: [PATCH 33/39] Add match existing user by and auto register settings and UI --- client/pages/config/authentication.vue | 100 ++++++++++++++-------- server/objects/settings/ServerSettings.js | 12 ++- 2 files changed, 74 insertions(+), 38 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 7cedfd25..0da486c1 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -12,45 +12,57 @@

OpenID Connect Authentication

-
- -
-
-
- -
-
- - auto_fix_high - Auto-populate -
+ + +
+
+
+
- - - - - - - - - - - - - - - - - -
- -

Auto Launch

-

Redirect to the auth provider automatically when navigating to the /login page

+
+ + auto_fix_high + Auto-populate
- -
+ + + + + + + + + + + + + + + + + +
+
+ +
+

Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider

+
+ +
+ +

Auto Launch

+

Redirect to the auth provider automatically when navigating to the login page

+
+ +
+ +

Auto Register

+

Automatically create new users after logging in

+
+
+
{{ $strings.ButtonSave }} @@ -90,6 +102,22 @@ export default { computed: { authMethods() { return this.authSettings.authActiveAuthMethods || [] + }, + matchingExistingOptions() { + return [ + { + text: 'Do not match', + value: null + }, + { + text: 'Match by email', + value: 'email' + }, + { + text: 'Match by username', + value: 'username' + } + ] } }, methods: { diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 781943b4..05a64d06 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -74,6 +74,8 @@ class ServerSettings { this.authOpenIDClientSecret = '' this.authOpenIDButtonText = 'Login with OpenId' this.authOpenIDAutoLaunch = false + this.authOpenIDAutoRegister = false + this.authOpenIDMatchExistingBy = null if (settings) { this.construct(settings) @@ -130,6 +132,8 @@ class ServerSettings { this.authOpenIDClientSecret = settings.authOpenIDClientSecret || '' this.authOpenIDButtonText = settings.authOpenIDButtonText || 'Login with OpenId' this.authOpenIDAutoLaunch = !!settings.authOpenIDAutoLaunch + this.authOpenIDAutoRegister = !!settings.authOpenIDAutoRegister + this.authOpenIDMatchExistingBy = settings.authOpenIDMatchExistingBy || null if (!Array.isArray(this.authActiveAuthMethods)) { this.authActiveAuthMethods = ['local'] @@ -234,7 +238,9 @@ class ServerSettings { authOpenIDClientID: this.authOpenIDClientID, // Do not return to client authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client authOpenIDButtonText: this.authOpenIDButtonText, - authOpenIDAutoLaunch: this.authOpenIDAutoLaunch + authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, + authOpenIDAutoRegister: this.authOpenIDAutoRegister, + authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy } } @@ -263,7 +269,9 @@ class ServerSettings { authOpenIDClientID: this.authOpenIDClientID, // Do not return to client authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client authOpenIDButtonText: this.authOpenIDButtonText, - authOpenIDAutoLaunch: this.authOpenIDAutoLaunch + authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, + authOpenIDAutoRegister: this.authOpenIDAutoRegister, + authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy } } From ee75d672e6d46c324f817e949aea72cd487d04b4 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 8 Nov 2023 16:14:57 -0600 Subject: [PATCH 34/39] Matching user by openid sub, email or username based on server settings. Auto register user. Persist sub on User records --- server/Auth.js | 53 ++++++++++++++++--- server/models/User.js | 102 +++++++++++++++++++++++++++++++----- server/objects/user/User.js | 9 +++- 3 files changed, 142 insertions(+), 22 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 361380f8..eeb7ad47 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -82,14 +82,51 @@ class Auth { scope: 'openid profile email' } }, async (tokenset, userinfo, done) => { - // TODO: Here is where to lookup the Abs user or register a new Abs user Logger.debug(`[Auth] openid callback userinfo=`, userinfo) - let user = null - // TODO: Temporary lookup existing user by email. May be replaced by a setting to toggle this or use name - if (userinfo.email && userinfo.email_verified) { - user = await Database.userModel.getUserByEmail(userinfo.email) - // TODO: If using existing user then save userinfo.sub on user + if (!userinfo.sub) { + Logger.error(`[Auth] openid callback invalid userinfo, no sub`) + return done(null, null) + } + + // First check for matching user by sub + let user = await Database.userModel.getUserByOpenIDSub(userinfo.sub) + if (!user) { + // Optionally match existing by email or username based on server setting "authOpenIDMatchExistingBy" + if (Database.serverSettings.authOpenIDMatchExistingBy === 'email' && userinfo.email && userinfo.email_verified) { + Logger.info(`[Auth] openid: User not found, checking existing with email "${userinfo.email}"`) + user = await Database.userModel.getUserByEmail(userinfo.email) + // Check that user is not already matched + if (user?.authOpenIDSub) { + Logger.warn(`[Auth] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`) + // TODO: Show some error log? + user = null + } + } else if (Database.serverSettings.authOpenIDMatchExistingBy === 'username' && userinfo.preferred_username) { + Logger.info(`[Auth] openid: User not found, checking existing with username "${userinfo.preferred_username}"`) + user = await Database.userModel.getUserByUsername(userinfo.preferred_username) + // Check that user is not already matched + if (user?.authOpenIDSub) { + Logger.warn(`[Auth] openid: User found with username "${userinfo.preferred_username}" but is already matched with sub "${user.authOpenIDSub}"`) + // TODO: Show some error log? + user = null + } + } + + // If existing user was matched and isActive then save sub to user + if (user?.isActive) { + Logger.info(`[Auth] openid: New user found matching existing user "${user.username}"`) + user.authOpenIDSub = userinfo.sub + await Database.userModel.updateFromOld(user) + } else if (user && !user.isActive) { + Logger.warn(`[Auth] openid: New user found matching existing user "${user.username}" but that user is deactivated`) + } + + // Optionally auto register the user + if (!user && Database.serverSettings.authOpenIDAutoRegister) { + Logger.info(`[Auth] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo) + user = await Database.userModel.createUserFromOpenIdUserInfo(userinfo, this) + } } if (!user?.isActive) { @@ -368,7 +405,7 @@ class Auth { /** * Function to generate a jwt token for a given user * - * @param {Object} user + * @param {{ id:string, username:string }} user * @returns {string} token */ generateAccessToken(user) { @@ -405,7 +442,7 @@ class Auth { const users = await Database.userModel.getOldUsers() if (users.length) { for (const user of users) { - user.token = await this.generateAccessToken({ userId: user.id, username: user.username }) + user.token = await this.generateAccessToken(user) } await Database.updateBulkUsers(users) } diff --git a/server/models/User.js b/server/models/User.js index d3028d9a..4c348f42 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -1,7 +1,9 @@ const uuidv4 = require("uuid").v4 -const { DataTypes, Model, Op } = require('sequelize') +const sequelize = require('sequelize') const Logger = require('../Logger') const oldUser = require('../objects/user/User') +const SocketAuthority = require('../SocketAuthority') +const { DataTypes, Model } = sequelize class User extends Model { constructor(values, options) { @@ -46,6 +48,12 @@ class User extends Model { return users.map(u => this.getOldUser(u)) } + /** + * Get old user model from new + * + * @param {Object} userExpanded + * @returns {oldUser} + */ static getOldUser(userExpanded) { const mediaProgress = userExpanded.mediaProgresses.map(mp => mp.getOldMediaProgress()) @@ -72,15 +80,27 @@ class User extends Model { createdAt: userExpanded.createdAt.valueOf(), permissions, librariesAccessible, - itemTagsSelected + itemTagsSelected, + authOpenIDSub: userExpanded.extraData?.authOpenIDSub || null }) } + /** + * + * @param {oldUser} oldUser + * @returns {Promise} + */ static createFromOld(oldUser) { const user = this.getFromOld(oldUser) return this.create(user) } + /** + * Update User from old user model + * + * @param {oldUser} oldUser + * @returns {Promise} + */ static updateFromOld(oldUser) { const user = this.getFromOld(oldUser) return this.update(user, { @@ -93,7 +113,21 @@ class User extends Model { }) } + /** + * Get new User model from old + * + * @param {oldUser} oldUser + * @returns {Object} + */ static getFromOld(oldUser) { + const extraData = { + seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [], + oldUserId: oldUser.oldUserId + } + if (oldUser.authOpenIDSub) { + extraData.authOpenIDSub = oldUser.authOpenIDSub + } + return { id: oldUser.id, username: oldUser.username, @@ -103,10 +137,7 @@ class User extends Model { token: oldUser.token || null, isActive: !!oldUser.isActive, lastSeen: oldUser.lastSeen || null, - extraData: { - seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [], - oldUserId: oldUser.oldUserId - }, + extraData, createdAt: oldUser.createdAt || Date.now(), permissions: { ...oldUser.permissions, @@ -130,12 +161,12 @@ class User extends Model { * @param {string} username * @param {string} pash * @param {Auth} auth - * @returns {oldUser} + * @returns {Promise} */ static async createRootUser(username, pash, auth) { const userId = uuidv4() - const token = await auth.generateAccessToken({ userId, username }) + const token = await auth.generateAccessToken({ id: userId, username }) const newRoot = new oldUser({ id: userId, @@ -150,6 +181,38 @@ class User extends Model { return newRoot } + /** + * Create user from openid userinfo + * @param {Object} userinfo + * @param {Auth} auth + * @returns {Promise} + */ + static async createUserFromOpenIdUserInfo(userinfo, auth) { + const userId = uuidv4() + // TODO: Ensure username is unique? + const username = userinfo.preferred_username || userinfo.name || userinfo.sub + const email = (userinfo.email && userinfo.email_verified) ? userinfo.email : null + + const token = await auth.generateAccessToken({ id: userId, username }) + + const newUser = new oldUser({ + id: userId, + type: 'user', + username, + email, + pash: null, + token, + isActive: true, + authOpenIDSub: userinfo.sub, + createdAt: Date.now() + }) + if (await this.createFromOld(newUser)) { + SocketAuthority.adminEmitter('user_added', newUser.toJSONForBrowser()) + return newUser + } + return null + } + /** * Get a user by id or by the old database id * @temp User ids were updated in v2.3.0 migration and old API tokens may still use that id @@ -160,13 +223,13 @@ class User extends Model { if (!userId) return null const user = await this.findOne({ where: { - [Op.or]: [ + [sequelize.Op.or]: [ { id: userId }, { extraData: { - [Op.substring]: userId + [sequelize.Op.substring]: userId } } ] @@ -187,7 +250,7 @@ class User extends Model { const user = await this.findOne({ where: { username: { - [Op.like]: username + [sequelize.Op.like]: username } }, include: this.sequelize.models.mediaProgress @@ -206,7 +269,7 @@ class User extends Model { const user = await this.findOne({ where: { email: { - [Op.like]: email + [sequelize.Op.like]: email } }, include: this.sequelize.models.mediaProgress @@ -229,6 +292,21 @@ class User extends Model { return this.getOldUser(user) } + /** + * Get user by openid sub + * @param {string} sub + * @returns {Promise} returns null if not found + */ + static async getUserByOpenIDSub(sub) { + if (!sub) return null + const user = await this.findOne({ + where: sequelize.where(sequelize.literal(`extraData->>"authOpenIDSub"`), sub), + include: this.sequelize.models.mediaProgress + }) + if (!user) return null + return this.getOldUser(user) + } + /** * Get array of user id and username * @returns {object[]} { id, username } diff --git a/server/objects/user/User.js b/server/objects/user/User.js index 5192752a..b503872d 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -24,6 +24,8 @@ class User { this.librariesAccessible = [] // Library IDs (Empty if ALL libraries) this.itemTagsSelected = [] // Empty if ALL item tags accessible + this.authOpenIDSub = null + if (user) { this.construct(user) } @@ -66,7 +68,7 @@ class User { getDefaultUserPermissions() { return { download: true, - update: true, + update: this.type === 'root' || this.type === 'admin', delete: this.type === 'root', upload: this.type === 'root' || this.type === 'admin', accessAllLibraries: true, @@ -93,7 +95,8 @@ class User { createdAt: this.createdAt, permissions: this.permissions, librariesAccessible: [...this.librariesAccessible], - itemTagsSelected: [...this.itemTagsSelected] + itemTagsSelected: [...this.itemTagsSelected], + authOpenIDSub: this.authOpenIDSub } } @@ -186,6 +189,8 @@ class User { this.librariesAccessible = [...(user.librariesAccessible || [])] this.itemTagsSelected = [...(user.itemTagsSelected || [])] + + this.authOpenIDSub = user.authOpenIDSub || null } update(payload) { From 237fe84c54341b788c99a099b1f6018698074c2f Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 10 Nov 2023 16:11:51 -0600 Subject: [PATCH 35/39] Add new API endpoint for updating auth-settings and update passport auth strategies --- client/pages/config/authentication.vue | 20 ++- server/Auth.js | 196 +++++++++++++--------- server/controllers/MiscController.js | 88 +++++++++- server/objects/settings/ServerSettings.js | 68 ++++---- server/routers/ApiRouter.js | 2 + 5 files changed, 255 insertions(+), 119 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 0da486c1..9ea8172a 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -199,13 +199,19 @@ export default { if (this.enableOpenIDAuth) this.newAuthSettings.authActiveAuthMethods.push('openid') this.savingSettings = true - const success = await this.$store.dispatch('updateServerSettings', this.newAuthSettings) - this.savingSettings = false - if (success) { - this.$toast.success('Server settings updated') - } else { - this.$toast.error('Failed to update server settings') - } + this.$axios + .$patch('/api/auth-settings', this.newAuthSettings) + .then((data) => { + this.$store.commit('setServerSettings', data.serverSettings) + this.$toast.success('Server settings updated') + }) + .catch((error) => { + console.error('Failed to update server settings', error) + this.$toast.error('Failed to update server settings') + }) + .finally(() => { + this.savingSettings = false + }) }, init() { this.newAuthSettings = { diff --git a/server/Auth.js b/server/Auth.js index eeb7ad47..15e36576 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -36,7 +36,12 @@ class Auth { async initPassportJs() { // Check if we should load the local strategy (username + password login) if (global.ServerSettings.authActiveAuthMethods.includes("local")) { - passport.use(new LocalStrategy(this.localAuthCheckUserPw.bind(this))) + this.initAuthStrategyPassword() + } + + // Check if we should load the openid strategy + if (global.ServerSettings.authActiveAuthMethods.includes("openid")) { + this.initAuthStrategyOpenID() } // Check if we should load the google-oauth20 strategy @@ -62,84 +67,6 @@ class Auth { }).bind(this))) } - // Check if we should load the openid strategy - if (global.ServerSettings.authActiveAuthMethods.includes("openid")) { - const openIdIssuerClient = new OpenIDClient.Issuer({ - issuer: global.ServerSettings.authOpenIDIssuerURL, - authorization_endpoint: global.ServerSettings.authOpenIDAuthorizationURL, - token_endpoint: global.ServerSettings.authOpenIDTokenURL, - userinfo_endpoint: global.ServerSettings.authOpenIDUserInfoURL, - jwks_uri: global.ServerSettings.authOpenIDJwksURL - }).Client - const openIdClient = new openIdIssuerClient({ - client_id: global.ServerSettings.authOpenIDClientID, - client_secret: global.ServerSettings.authOpenIDClientSecret - }) - passport.use('openid-client', new OpenIDClient.Strategy({ - client: openIdClient, - params: { - redirect_uri: '/auth/openid/callback', - scope: 'openid profile email' - } - }, async (tokenset, userinfo, done) => { - Logger.debug(`[Auth] openid callback userinfo=`, userinfo) - - if (!userinfo.sub) { - Logger.error(`[Auth] openid callback invalid userinfo, no sub`) - return done(null, null) - } - - // First check for matching user by sub - let user = await Database.userModel.getUserByOpenIDSub(userinfo.sub) - if (!user) { - // Optionally match existing by email or username based on server setting "authOpenIDMatchExistingBy" - if (Database.serverSettings.authOpenIDMatchExistingBy === 'email' && userinfo.email && userinfo.email_verified) { - Logger.info(`[Auth] openid: User not found, checking existing with email "${userinfo.email}"`) - user = await Database.userModel.getUserByEmail(userinfo.email) - // Check that user is not already matched - if (user?.authOpenIDSub) { - Logger.warn(`[Auth] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`) - // TODO: Show some error log? - user = null - } - } else if (Database.serverSettings.authOpenIDMatchExistingBy === 'username' && userinfo.preferred_username) { - Logger.info(`[Auth] openid: User not found, checking existing with username "${userinfo.preferred_username}"`) - user = await Database.userModel.getUserByUsername(userinfo.preferred_username) - // Check that user is not already matched - if (user?.authOpenIDSub) { - Logger.warn(`[Auth] openid: User found with username "${userinfo.preferred_username}" but is already matched with sub "${user.authOpenIDSub}"`) - // TODO: Show some error log? - user = null - } - } - - // If existing user was matched and isActive then save sub to user - if (user?.isActive) { - Logger.info(`[Auth] openid: New user found matching existing user "${user.username}"`) - user.authOpenIDSub = userinfo.sub - await Database.userModel.updateFromOld(user) - } else if (user && !user.isActive) { - Logger.warn(`[Auth] openid: New user found matching existing user "${user.username}" but that user is deactivated`) - } - - // Optionally auto register the user - if (!user && Database.serverSettings.authOpenIDAutoRegister) { - Logger.info(`[Auth] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo) - user = await Database.userModel.createUserFromOpenIdUserInfo(userinfo, this) - } - } - - if (!user?.isActive) { - // deny login - done(null, null) - return - } - - // permit login - return done(null, user) - })) - } - // Load the JwtStrategy (always) -> for bearer token auth passport.use(new JwtStrategy({ jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), ExtractJwt.fromUrlQueryParameter('token')]), @@ -167,6 +94,117 @@ class Auth { }).bind(this)) } + /** + * Passport use LocalStrategy + */ + initAuthStrategyPassword() { + passport.use(new LocalStrategy(this.localAuthCheckUserPw.bind(this))) + } + + /** + * Passport use OpenIDClient.Strategy + */ + initAuthStrategyOpenID() { + const openIdIssuerClient = new OpenIDClient.Issuer({ + issuer: global.ServerSettings.authOpenIDIssuerURL, + authorization_endpoint: global.ServerSettings.authOpenIDAuthorizationURL, + token_endpoint: global.ServerSettings.authOpenIDTokenURL, + userinfo_endpoint: global.ServerSettings.authOpenIDUserInfoURL, + jwks_uri: global.ServerSettings.authOpenIDJwksURL + }).Client + const openIdClient = new openIdIssuerClient({ + client_id: global.ServerSettings.authOpenIDClientID, + client_secret: global.ServerSettings.authOpenIDClientSecret + }) + passport.use('openid-client', new OpenIDClient.Strategy({ + client: openIdClient, + params: { + redirect_uri: '/auth/openid/callback', + scope: 'openid profile email' + } + }, async (tokenset, userinfo, done) => { + Logger.debug(`[Auth] openid callback userinfo=`, userinfo) + + if (!userinfo.sub) { + Logger.error(`[Auth] openid callback invalid userinfo, no sub`) + return done(null, null) + } + + // First check for matching user by sub + let user = await Database.userModel.getUserByOpenIDSub(userinfo.sub) + if (!user) { + // Optionally match existing by email or username based on server setting "authOpenIDMatchExistingBy" + if (Database.serverSettings.authOpenIDMatchExistingBy === 'email' && userinfo.email && userinfo.email_verified) { + Logger.info(`[Auth] openid: User not found, checking existing with email "${userinfo.email}"`) + user = await Database.userModel.getUserByEmail(userinfo.email) + // Check that user is not already matched + if (user?.authOpenIDSub) { + Logger.warn(`[Auth] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`) + // TODO: Show some error log? + user = null + } + } else if (Database.serverSettings.authOpenIDMatchExistingBy === 'username' && userinfo.preferred_username) { + Logger.info(`[Auth] openid: User not found, checking existing with username "${userinfo.preferred_username}"`) + user = await Database.userModel.getUserByUsername(userinfo.preferred_username) + // Check that user is not already matched + if (user?.authOpenIDSub) { + Logger.warn(`[Auth] openid: User found with username "${userinfo.preferred_username}" but is already matched with sub "${user.authOpenIDSub}"`) + // TODO: Show some error log? + user = null + } + } + + // If existing user was matched and isActive then save sub to user + if (user?.isActive) { + Logger.info(`[Auth] openid: New user found matching existing user "${user.username}"`) + user.authOpenIDSub = userinfo.sub + await Database.userModel.updateFromOld(user) + } else if (user && !user.isActive) { + Logger.warn(`[Auth] openid: New user found matching existing user "${user.username}" but that user is deactivated`) + } + + // Optionally auto register the user + if (!user && Database.serverSettings.authOpenIDAutoRegister) { + Logger.info(`[Auth] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo) + user = await Database.userModel.createUserFromOpenIdUserInfo(userinfo, this) + } + } + + if (!user?.isActive) { + // deny login + done(null, null) + return + } + + // permit login + return done(null, user) + })) + } + + /** + * Unuse strategy + * + * @param {string} name + */ + unuseAuthStrategy(name) { + passport.unuse(name) + } + + /** + * Use strategy + * + * @param {string} name + */ + useAuthStrategy(name) { + if (name === 'openid') { + this.initAuthStrategyOpenID() + } else if (name === 'local') { + this.initAuthStrategyPassword() + } else { + Logger.error('[Auth] Invalid auth strategy ' + name) + } + } + /** * Stores the client's choice how the login callback should happen in temp cookies * diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 9b58188f..6d7507cf 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -129,7 +129,7 @@ class MiscController { return res.sendStatus(403) } const settingsUpdate = req.body - if (!settingsUpdate || !isObject(settingsUpdate)) { + if (!isObject(settingsUpdate)) { return res.status(400).send('Invalid settings update object') } @@ -604,5 +604,91 @@ class MiscController { } return res.json(Database.serverSettings.authenticationSettings) } + + /** + * PATCH: api/auth-settings + * @this import('../routers/ApiRouter') + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async updateAuthSettings(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to update auth settings`) + return res.sendStatus(403) + } + + const settingsUpdate = req.body + if (!isObject(settingsUpdate)) { + return res.status(400).send('Invalid auth settings update object') + } + + let hasUpdates = false + + const currentAuthenticationSettings = Database.serverSettings.authenticationSettings + const originalAuthMethods = [...currentAuthenticationSettings.authActiveAuthMethods] + + // TODO: Better validation of auth settings once auth settings are separated from server settings + for (const key in currentAuthenticationSettings) { + if (settingsUpdate[key] === undefined) continue + + if (key === 'authActiveAuthMethods') { + let updatedAuthMethods = settingsUpdate[key]?.filter?.((authMeth) => Database.serverSettings.supportedAuthMethods.includes(authMeth)) + if (Array.isArray(updatedAuthMethods) && updatedAuthMethods.length) { + updatedAuthMethods.sort() + currentAuthenticationSettings[key].sort() + if (updatedAuthMethods.join() !== currentAuthenticationSettings[key].join()) { + Logger.debug(`[MiscController] Updating auth settings key "authActiveAuthMethods" from "${currentAuthenticationSettings[key].join()}" to "${updatedAuthMethods.join()}"`) + Database.serverSettings[key] = updatedAuthMethods + hasUpdates = true + } + } else { + Logger.warn(`[MiscController] Invalid value for authActiveAuthMethods`) + } + } else { + const updatedValueType = typeof settingsUpdate[key] + if (['authOpenIDAutoLaunch', 'authOpenIDAutoRegister'].includes(key)) { + if (updatedValueType !== 'boolean') { + Logger.warn(`[MiscController] Invalid value for ${key}. Expected boolean`) + continue + } + } else if (updatedValueType !== null && updatedValueType !== 'string') { + Logger.warn(`[MiscController] Invalid value for ${key}. Expected string or null`) + continue + } + let updatedValue = settingsUpdate[key] + if (updatedValue === '') updatedValue = null + let currentValue = currentAuthenticationSettings[key] + if (currentValue === '') currentValue = null + + if (updatedValue !== currentValue) { + Logger.debug(`[MiscController] Updating auth settings key "${key}" from "${currentValue}" to "${updatedValue}"`) + Database.serverSettings[key] = updatedValue + hasUpdates = true + } + } + } + + if (hasUpdates) { + // Use/unuse auth methods + Database.serverSettings.supportedAuthMethods.forEach((authMethod) => { + if (originalAuthMethods.includes(authMethod) && !Database.serverSettings.authActiveAuthMethods.includes(authMethod)) { + // Auth method has been removed + Logger.info(`[MiscController] Disabling active auth method "${authMethod}"`) + this.auth.unuseAuthStrategy(authMethod) + } else if (!originalAuthMethods.includes(authMethod) && Database.serverSettings.authActiveAuthMethods.includes(authMethod)) { + // Auth method has been added + Logger.info(`[MiscController] Enabling active auth method "${authMethod}"`) + this.auth.useAuthStrategy(authMethod) + } + }) + + await Database.updateServerSettings() + } + + res.json({ + serverSettings: Database.serverSettings.toJSONForBrowser() + }) + } } module.exports = new MiscController() \ No newline at end of file diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 05a64d06..afde1ddf 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -59,19 +59,19 @@ class ServerSettings { this.authActiveAuthMethods = ['local'] // google-oauth20 settings - this.authGoogleOauth20ClientID = '' - this.authGoogleOauth20ClientSecret = '' - this.authGoogleOauth20CallbackURL = '' + this.authGoogleOauth20ClientID = null + this.authGoogleOauth20ClientSecret = null + this.authGoogleOauth20CallbackURL = null // openid settings - this.authOpenIDIssuerURL = '' - this.authOpenIDAuthorizationURL = '' - this.authOpenIDTokenURL = '' - this.authOpenIDUserInfoURL = '' - this.authOpenIDJwksURL = '' - this.authOpenIDLogoutURL = '' - this.authOpenIDClientID = '' - this.authOpenIDClientSecret = '' + this.authOpenIDIssuerURL = null + this.authOpenIDAuthorizationURL = null + this.authOpenIDTokenURL = null + this.authOpenIDUserInfoURL = null + this.authOpenIDJwksURL = null + this.authOpenIDLogoutURL = null + this.authOpenIDClientID = null + this.authOpenIDClientSecret = null this.authOpenIDButtonText = 'Login with OpenId' this.authOpenIDAutoLaunch = false this.authOpenIDAutoRegister = false @@ -118,18 +118,18 @@ class ServerSettings { this.buildNumber = settings.buildNumber || 0 // Added v2.4.5 this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local'] - this.authGoogleOauth20ClientID = settings.authGoogleOauth20ClientID || '' - this.authGoogleOauth20ClientSecret = settings.authGoogleOauth20ClientSecret || '' - this.authGoogleOauth20CallbackURL = settings.authGoogleOauth20CallbackURL || '' + this.authGoogleOauth20ClientID = settings.authGoogleOauth20ClientID || null + this.authGoogleOauth20ClientSecret = settings.authGoogleOauth20ClientSecret || null + this.authGoogleOauth20CallbackURL = settings.authGoogleOauth20CallbackURL || null - this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || '' - this.authOpenIDAuthorizationURL = settings.authOpenIDAuthorizationURL || '' - this.authOpenIDTokenURL = settings.authOpenIDTokenURL || '' - this.authOpenIDUserInfoURL = settings.authOpenIDUserInfoURL || '' - this.authOpenIDJwksURL = settings.authOpenIDJwksURL || '' - this.authOpenIDLogoutURL = settings.authOpenIDLogoutURL || '' - this.authOpenIDClientID = settings.authOpenIDClientID || '' - this.authOpenIDClientSecret = settings.authOpenIDClientSecret || '' + this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || null + this.authOpenIDAuthorizationURL = settings.authOpenIDAuthorizationURL || null + this.authOpenIDTokenURL = settings.authOpenIDTokenURL || null + this.authOpenIDUserInfoURL = settings.authOpenIDUserInfoURL || null + this.authOpenIDJwksURL = settings.authOpenIDJwksURL || null + this.authOpenIDLogoutURL = settings.authOpenIDLogoutURL || null + this.authOpenIDClientID = settings.authOpenIDClientID || null + this.authOpenIDClientSecret = settings.authOpenIDClientSecret || null this.authOpenIDButtonText = settings.authOpenIDButtonText || 'Login with OpenId' this.authOpenIDAutoLaunch = !!settings.authOpenIDAutoLaunch this.authOpenIDAutoRegister = !!settings.authOpenIDAutoRegister @@ -142,9 +142,9 @@ class ServerSettings { // remove uninitialized methods // GoogleOauth20 if (this.authActiveAuthMethods.includes('google-oauth20') && ( - this.authGoogleOauth20ClientID === '' || - this.authGoogleOauth20ClientSecret === '' || - this.authGoogleOauth20CallbackURL === '' + !this.authGoogleOauth20ClientID || + !this.authGoogleOauth20ClientSecret || + !this.authGoogleOauth20CallbackURL )) { this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('google-oauth20', 0), 1) } @@ -152,13 +152,13 @@ class ServerSettings { // remove uninitialized methods // OpenID if (this.authActiveAuthMethods.includes('openid') && ( - this.authOpenIDIssuerURL === '' || - this.authOpenIDAuthorizationURL === '' || - this.authOpenIDTokenURL === '' || - this.authOpenIDUserInfoURL === '' || - this.authOpenIDJwksURL === '' || - this.authOpenIDClientID === '' || - this.authOpenIDClientSecret === '' + !this.authOpenIDIssuerURL || + !this.authOpenIDAuthorizationURL || + !this.authOpenIDTokenURL || + !this.authOpenIDUserInfoURL || + !this.authOpenIDJwksURL || + !this.authOpenIDClientID || + !this.authOpenIDClientSecret )) { this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('openid', 0), 1) } @@ -254,6 +254,10 @@ class ServerSettings { return json } + get supportedAuthMethods() { + return ['local', 'openid'] + } + get authenticationSettings() { return { authActiveAuthMethods: this.authActiveAuthMethods, diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index c4c0df5e..8c97d59b 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -35,6 +35,7 @@ const Series = require('../objects/entities/Series') class ApiRouter { constructor(Server) { + /** @type {import('../Auth')} */ this.auth = Server.auth this.playbackSessionManager = Server.playbackSessionManager this.abMergeManager = Server.abMergeManager @@ -310,6 +311,7 @@ class ApiRouter { this.router.delete('/genres/:genre', MiscController.deleteGenre.bind(this)) this.router.post('/validate-cron', MiscController.validateCronExpression.bind(this)) this.router.get('/auth-settings', MiscController.getAuthSettings.bind(this)) + this.router.patch('/auth-settings', MiscController.updateAuthSettings.bind(this)) this.router.post('/watcher/update', MiscController.updateWatchedPath.bind(this)) } From 557ef2ef798daf5d18b9206de586087b7f389de2 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 11 Nov 2023 10:52:05 -0600 Subject: [PATCH 36/39] Update /auth/openid endpoints for correct PKCE handling - Provide error handling for /auth/openid - Add session.mobile inside /auth/openid - Proper PKCE handling for /auth/openid/callback - redirect_uri handling for the token url in /auth/openid/callback Co-authored-by: Denis Arnst --- server/Auth.js | 154 +++++++++++++++++++++++++++++-------------------- 1 file changed, 90 insertions(+), 64 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 15e36576..54fa52a4 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -306,82 +306,108 @@ class Auth { // openid strategy login route (this redirects to the configured openid login provider) router.get('/auth/openid', (req, res, next) => { - // helper function from openid-client - function pick(object, ...paths) { - const obj = {} - for (const path of paths) { - if (object[path] !== undefined) { - obj[path] = object[path] + try { + // helper function from openid-client + function pick(object, ...paths) { + const obj = {} + for (const path of paths) { + if (object[path] !== undefined) { + obj[path] = object[path] + } } + return obj } - return obj - } - // Get the OIDC client from the strategy - // We need to call the client manually, because the strategy does not support forwarding the code challenge - // for API or mobile clients - const oidcStrategy = passport._strategy('openid-client') - oidcStrategy._params.redirect_uri = new URL(`${req.protocol}://${req.get('host')}/auth/openid/callback`).toString() - const client = oidcStrategy._client - const sessionKey = oidcStrategy._key + // Get the OIDC client from the strategy + // We need to call the client manually, because the strategy does not support forwarding the code challenge + // for API or mobile clients + const oidcStrategy = passport._strategy('openid-client') + oidcStrategy._params.redirect_uri = new URL(`${req.protocol}://${req.get('host')}/auth/openid/callback`).toString() + const client = oidcStrategy._client + const sessionKey = oidcStrategy._key - let code_challenge - let code_challenge_method + let code_challenge + let code_challenge_method - // If code_challenge is provided, expect that code_verifier will be handled by the client (mobile app) - // The web frontend of ABS does not need to do a PKCE itself, because it never handles the "code" of the oauth flow - // and as such will not send a code challenge, we will generate then one - if (req.query.code_challenge) { - code_challenge = req.query.code_challenge - code_challenge_method = req.query.code_challenge_method || 'S256' + // If code_challenge is provided, expect that code_verifier will be handled by the client (mobile app) + // The web frontend of ABS does not need to do a PKCE itself, because it never handles the "code" of the oauth flow + // and as such will not send a code challenge, we will generate then one + if (req.query.code_challenge) { + code_challenge = req.query.code_challenge + code_challenge_method = req.query.code_challenge_method || 'S256' - if (!['S256', 'plain'].includes(code_challenge_method)) { - return res.status(400).send('Invalid code_challenge_method') + if (!['S256', 'plain'].includes(code_challenge_method)) { + return res.status(400).send('Invalid code_challenge_method') + } + } else { + // If no code_challenge is provided, assume a web application flow and generate one + const code_verifier = OpenIDClient.generators.codeVerifier() + code_challenge = OpenIDClient.generators.codeChallenge(code_verifier) + code_challenge_method = 'S256' + + // Store the code_verifier in the session for later use in the token exchange + req.session[sessionKey] = { ...req.session[sessionKey], code_verifier } } - } else { - // If no code_challenge is provided, assume a web application flow and generate one - const code_verifier = OpenIDClient.generators.codeVerifier() - code_challenge = OpenIDClient.generators.codeChallenge(code_verifier) - code_challenge_method = 'S256' - // Store the code_verifier in the session for later use in the token exchange - req.session[sessionKey] = { ...req.session[sessionKey], code_verifier } + const params = { + state: OpenIDClient.generators.random(), + // Other params by the passport strategy + ...oidcStrategy._params + } + + if (!params.nonce && params.response_type.includes('id_token')) { + params.nonce = OpenIDClient.generators.random() + } + + req.session[sessionKey] = { + ...req.session[sessionKey], + ...pick(params, 'nonce', 'state', 'max_age', 'response_type') + } + + // Now get the URL to direct to + const authorizationUrl = client.authorizationUrl({ + ...params, + scope: 'openid profile email', + response_type: 'code', + code_challenge, + code_challenge_method, + }) + + // params (isRest, callback) to a cookie that will be send to the client + this.paramsToCookies(req, res) + + // Redirect the user agent (browser) to the authorization URL + res.redirect(authorizationUrl) + } catch (error) { + Logger.error(`[Auth] Error in /auth/openid route: ${error}`) + res.status(500).send('Internal Server Error') } - - const params = { - state: OpenIDClient.generators.random(), - // Other params by the passport strategy - ...oidcStrategy._params - } - - if (!params.nonce && params.response_type.includes('id_token')) { - params.nonce = OpenIDClient.generators.random() - } - - req.session[sessionKey] = { - ...req.session[sessionKey], - ...pick(params, 'nonce', 'state', 'max_age', 'response_type') - } - - // Now get the URL to direct to - const authorizationUrl = client.authorizationUrl({ - ...params, - scope: 'openid profile email', - response_type: 'code', - code_challenge, - code_challenge_method, - }) - - // params (isRest, callback) to a cookie that will be send to the client - this.paramsToCookies(req, res) - - // Redirect the user agent (browser) to the authorization URL - res.redirect(authorizationUrl) }) // openid strategy callback route (this receives the token from the configured openid login provider) - router.get('/auth/openid/callback', - passport.authenticate('openid-client'), + router.get('/auth/openid/callback', (req, res, next) => { + const oidcStrategy = passport._strategy('openid-client') + const sessionKey = oidcStrategy._key + + if (!req.session[sessionKey]) { + return res.status(400).send('No session') + } + + // If the client sends us a code_verifier, we will tell passport to use this to send this in the token request + // The code_verifier will be validated by the oauth2 provider by comparing it to the code_challenge in the first request + // Crucial for API/Mobile clients + if (req.query.code_verifier) { + req.session[sessionKey].code_verifier = req.query.code_verifier + } + + // While not required by the standard, the passport plugin re-sends the original redirect_uri in the token request + // We need to set it correctly, as some SSO providers (e.g. keycloak) check that parameter when it is provided + if (req.session[sessionKey].mobile) { + return passport.authenticate('openid-client', { redirect_uri: 'audiobookshelf://oauth' })(req, res, next) + } else { + return passport.authenticate('openid-client')(req, res, next) + } + }, // on a successfull login: read the cookies and react like the client requested (callback or json) this.handleLoginSuccessBasedOnCookie.bind(this)) From 1ad6722e6d5fb73fa754d64fc3be7e9f9205fcce Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 11 Nov 2023 11:29:59 -0600 Subject: [PATCH 37/39] Remove google-oauth passport strategy --- client/assets/app.css | 20 +++++++++ client/components/ui/Btn.vue | 24 +---------- client/pages/login.vue | 19 ++------ package-lock.json | 84 ------------------------------------ package.json | 1 - server/Auth.js | 39 ----------------- 6 files changed, 25 insertions(+), 162 deletions(-) diff --git a/client/assets/app.css b/client/assets/app.css index b7b8499d..1a83dc1c 100644 --- a/client/assets/app.css +++ b/client/assets/app.css @@ -258,4 +258,24 @@ Bookshelf Label .no-bars .Vue-Toastification__container.top-right { padding-top: 8px; +} + +.abs-btn::before { + content: ''; + position: absolute; + border-radius: 6px; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0); + transition: all 0.1s ease-in-out; +} + +.abs-btn:hover:not(:disabled)::before { + background-color: rgba(255, 255, 255, 0.1); +} + +.abs-btn:disabled::before { + background-color: rgba(0, 0, 0, 0.2); } \ No newline at end of file diff --git a/client/components/ui/Btn.vue b/client/components/ui/Btn.vue index d9b75715..7f73a956 100644 --- a/client/components/ui/Btn.vue +++ b/client/components/ui/Btn.vue @@ -1,5 +1,5 @@