From 464a1ea1213b0f4dda221ad6abfcdb9cfcc5c301 Mon Sep 17 00:00:00 2001 From: "Brian C. Arnold" Date: Wed, 21 Dec 2022 12:12:51 -0500 Subject: [PATCH] Added LDAP Auth Implementation --- index.js | 14 ++- package-lock.json | 216 ++++++++++++++++++++++++++++++++++- package.json | 3 +- server/Auth.js | 112 +++++++++++++++--- server/Server.js | 15 ++- server/authProviders/ldap.js | 124 ++++++++++++++++++++ 6 files changed, 459 insertions(+), 25 deletions(-) create mode 100644 server/authProviders/ldap.js diff --git a/index.js b/index.js index 585af22ef..bcdf91785 100644 --- a/index.js +++ b/index.js @@ -18,12 +18,20 @@ const PORT = process.env.PORT || 80 const HOST = process.env.HOST const CONFIG_PATH = process.env.CONFIG_PATH || '/config' const METADATA_PATH = process.env.METADATA_PATH || '/metadata' -const UID = process.env.AUDIOBOOKSHELF_UID -const GID = process.env.AUDIOBOOKSHELF_GID +const UID = process.env.AUDIOBOOKSHELF_UID || 99 +const GID = process.env.AUDIOBOOKSHELF_GID || 100 + +const LDAP_URL = process.env.LDAP_URL +const LDAP_BASE_DN = process.env.LDAP_BASE_DN +const LDAP_BIND_USER = process.env.LDAP_BIND_USER +const LDAP_BIND_PASS = process.env.LDAP_BIND_PASS +const LDAP_SEARCH_FILTER = process.env.LDAP_SEARCH_FILTER +const LDAP_USERNAME_ATTRIBUTE = process.env.LDAP_USERNAME_ATTRIBUTE +const LDAP_ENABLED = process.env.LDAP_ENABLED const SOURCE = process.env.SOURCE || 'docker' const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || '' console.log('Config', CONFIG_PATH, METADATA_PATH) -const Server = new server(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) +const Server = new server(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH, LDAP_ENABLED, LDAP_URL, LDAP_BASE_DN, LDAP_BIND_USER, LDAP_BIND_PASS, LDAP_SEARCH_FILTER, LDAP_USERNAME_ATTRIBUTE) Server.start() diff --git a/package-lock.json b/package-lock.json index e906af737..e4830913f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "express": "^4.17.1", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", + "ldapjs": "^2.3.3", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", "sequelize": "^6.32.1", @@ -174,6 +175,11 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -311,6 +317,22 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -325,6 +347,17 @@ "form-data": "^4.0.0" } }, + "node_modules/backoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", + "integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==", + "dependencies": { + "precond": "0.2" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -545,6 +578,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -835,6 +873,14 @@ "node": ">= 0.10.0" } }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "engines": [ + "node >=0.6.0" + ] + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1308,6 +1354,35 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "optional": true }, + "node_modules/ldap-filter": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz", + "integrity": "sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg==", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ldapjs": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.3.tgz", + "integrity": "sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==", + "dependencies": { + "abstract-logging": "^2.0.0", + "asn1": "^0.2.4", + "assert-plus": "^1.0.0", + "backoff": "^2.5.0", + "ldap-filter": "^0.3.3", + "once": "^1.4.0", + "vasync": "^2.2.0", + "verror": "^1.8.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -1870,6 +1945,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/precond": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", + "integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -2598,6 +2681,43 @@ "node": ">= 0.8" } }, + "node_modules/vasync": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz", + "integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "verror": "1.10.0" + } + }, + "node_modules/vasync/node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -2818,6 +2938,11 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, "accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2922,6 +3047,19 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2936,6 +3074,14 @@ "form-data": "^4.0.0" } }, + "backoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", + "integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==", + "requires": { + "precond": "0.2" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3102,6 +3248,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, "cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -3324,6 +3475,11 @@ "vary": "~1.1.2" } }, + "extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==" + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -3673,6 +3829,29 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "optional": true }, + "ldap-filter": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz", + "integrity": "sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg==", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "ldapjs": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.3.tgz", + "integrity": "sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==", + "requires": { + "abstract-logging": "^2.0.0", + "asn1": "^0.2.4", + "assert-plus": "^1.0.0", + "backoff": "^2.5.0", + "ldap-filter": "^0.3.3", + "once": "^1.4.0", + "vasync": "^2.2.0", + "verror": "^1.8.1" + } + }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -4080,6 +4259,11 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "precond": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", + "integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==" + }, "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -4602,6 +4786,36 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, + "vasync": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz", + "integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==", + "requires": { + "verror": "1.10.0" + }, + "dependencies": { + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + } + } + }, + "verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -4672,4 +4886,4 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 1fcd6d603..dd556de2b 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "express": "^4.17.1", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", + "ldapjs": "^2.3.3", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", "sequelize": "^6.32.1", @@ -44,4 +45,4 @@ "devDependencies": { "nodemon": "^2.0.20" } -} \ No newline at end of file +} diff --git a/server/Auth.js b/server/Auth.js index c9d67b200..a925d5595 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -1,11 +1,28 @@ const bcrypt = require('./libs/bcryptjs') +const ldap = require('./authProviders/ldap') const jwt = require('./libs/jsonwebtoken') const requestIp = require('./libs/requestIp') +const User = require('./objects/user/User') +const { getId } = require('./utils') const Logger = require('./Logger') const Database = require('./Database') class Auth { - constructor() { } + + constructor() { + if (global.ldapEnabled) { + this.ldap = ldap + } + this.user = null + } + + get username() { + return this.user ? this.user.username : 'nobody' + } + + get users() { + return Database.users + } cors(req, res, next) { res.header('Access-Control-Allow-Origin', '*') @@ -34,15 +51,18 @@ class Auth { // New token secret creation added in v2.1.0 so generate new API tokens for each user if (Database.users.length) { for (const user of Database.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`) + if (user.token) { + delete user.token + } + //Tokens should never be stored on the server, the entire point is that the server can cryptographically verify the token without storing it + Logger.warn(`[Auth] User ${user.username} api token security hole has been removed`) } await Database.updateBulkUsers(Database.users) } } async authMiddleware(req, res, next) { - var token = null + let token = null // If using a get request, the token can be passed as a query string if (req.method === 'GET' && req.query && req.query.token) { @@ -104,7 +124,16 @@ class Auth { }) } - getUserLoginResponsePayload(user) { + async getLdapUsers() { + if (this.ldap) { + const ldapUsers = await this.ldap.findAllUsers(); + return ldapUsers; + } else { + return []; + } + } + async getUserLoginResponsePayload(user) { + user.token = await this.generateAccessToken({ userId: user.id, username: user.username }); return { user: user.toJSONForBrowser(), userDefaultLibraryId: user.getDefaultLibraryId(Database.libraries), @@ -119,7 +148,28 @@ class Auth { const username = (req.body.username || '').toLowerCase() const password = req.body.password || '' - const user = Database.users.find(u => u.username.toLowerCase() === username) + let user = Database.users.find(u => u.username.toLowerCase() === username) + if (!user && this.ldap) { + Logger.warn(`[Auth] Could not find user in database from ${ipAddress}, checking LDAP...`) + const ldapUsers = await this.getLdapUsers() + Logger.debug(ldapUsers); + if (ldapUsers.filter(u => u[global.ldapUsernameAttribute].toLowerCase() == username.toLowerCase())) { + const ldapUser = ldapUsers.find(v => v[global.ldapUsernameAttribute].toLowerCase() == username.toLowerCase()) + const newUser = new User({ + username: ldapUser[global.ldapUsernameAttribute], + id: getId('usr'), + isActive: true, + isLocked: false, + librariesAccessible: [], + itemTagsAccessible: [], + type: 'ldap' + }) + const success = await Database.insertEntity('user', newUser) + if (success) { + user = Database.users.find(u => u.username.toLowerCase() === username) + } + } + } if (!user?.isActive) { Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`) @@ -136,22 +186,46 @@ class Auth { return res.status(401).send('Invalid root password (hint: there is none)') } else { Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`) - return res.json(this.getUserLoginResponsePayload(user)) + return res.json(await this.getUserLoginResponsePayload(user)) } } - // Check password match - const compare = await bcrypt.compare(password, user.pash) - if (compare) { - Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`) - 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`})`) + if (!user.pash || user.pash === '') { + // Check LDAP for user. + + const ldapUsers = await this.getLdapUsers() + const ldapUser = ldapUsers.find(v => v[global.ldapUsernameAttribute].toLowerCase() == username.toLowerCase()) + if (ldapUser != null) { + const adUserName = ldapUser.dn + const authResult = await this.ldap.authenticateUser(adUserName, password); + if (authResult) { + res.json(await 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') + } + } else { + return res.status(401).send('Invalid user or password') + } + + } else { + // Check password match + const compare = await bcrypt.compare(password, user.pash) + if (compare) { + Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`) + res.json(await 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') } - return res.status(401).send('Invalid user or password') } } @@ -162,7 +236,7 @@ class Auth { } async userChangePassword(req, res) { - var { password, newPassword } = req.body + const { password, newPassword } = req.body newPassword = newPassword || '' const matchingUser = Database.users.find(u => u.id === req.user.id) diff --git a/server/Server.js b/server/Server.js index 38a010220..580edd9af 100644 --- a/server/Server.js +++ b/server/Server.js @@ -38,7 +38,7 @@ const CronManager = require('./managers/CronManager') const TaskManager = require('./managers/TaskManager') class Server { - constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) { + constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH, LDAP_ENABLED, LDAP_URL, LDAP_BASE_DN, LDAP_BIND_USER, LDAP_BIND_PASS, LDAP_SEARCH_FILTER, LDAP_USERNAME_ATTRIBUTE) { this.Port = PORT this.Host = HOST global.Source = SOURCE @@ -49,6 +49,19 @@ class Server { global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH)) global.RouterBasePath = ROUTER_BASE_PATH global.XAccel = process.env.USE_X_ACCEL + global.ldapUrl = LDAP_URL + global.ldapBaseDn = LDAP_BASE_DN + global.ldapBindUser = LDAP_BIND_USER + global.ldapBindPass = LDAP_BIND_PASS + global.ldapSearchFilter = LDAP_SEARCH_FILTER + global.ldapUsernameAttribute = LDAP_USERNAME_ATTRIBUTE + global.ldapEnabled = LDAP_ENABLED + + // Fix backslash if not on Windows + if (process.platform !== 'win32') { + global.ConfigPath = global.ConfigPath.replace(/\\/g, '/') + global.MetadataPath = global.MetadataPath.replace(/\\/g, '/') + } if (!fs.pathExistsSync(global.ConfigPath)) { fs.mkdirSync(global.ConfigPath) diff --git a/server/authProviders/ldap.js b/server/authProviders/ldap.js new file mode 100644 index 000000000..4e93f77fe --- /dev/null +++ b/server/authProviders/ldap.js @@ -0,0 +1,124 @@ +const ldap = require('ldapjs'); +const Logger = require('../Logger'); +const { res } = require('../libs/dateAndTime'); +const { integer } = require('../libs/requestIp/isJs'); + +class LdapAuth { + constructor() {} + async getClient() { + return await new Promise((resolve, reject) => { + Logger.debug('Connecting...') + const client = ldap.createClient({ + url: global.ldapUrl, + bindDN: global.ldapBindUser, + bindCredentials: global.ldapBindPass + }); + let onError, onConnect, onTimeout; + onError = err => { + Logger.error('Error Connecting.') + client.removeListener('connect', onConnect); + reject(err); + }; + onConnect = () => { + Logger.debug('Connected.') + Logger.debug('Binding...') + client.removeListener('error', onError); + client.bind(global.ldapBindUser, global.ldapBindPass, err => { + if (err) { + Logger.error('Error Binding.') + reject(err); + } + else { + Logger.debug('Bound.') + resolve(client); + } + + // client.search + }) + }; + onTimeout = () => { + Logger.error('Timeout Connecting.') + reject(new Error('LDAP connection timeout')); + } + client.once('connect', onConnect) + client.once('error', onError) + client.once('connectTimeout', onTimeout) + }) + } + async findAllUsers() { + const client = await this.getClient(); + return await new Promise((resolve, reject) => { + Logger.info('Searching...') + Logger.debug(`global.ldapBaseDn: ${global.ldapBaseDn}`) + Logger.debug(`global.ldapSearchFilter: ${global.ldapSearchFilter}`) + client.search(global.ldapBaseDn, { + scope: 'sub', + filter: global.ldapSearchFilter + }, (err, res) => { + Logger.info('Search returned.') + const items = [] + Logger.debug('Attaching to entry events...') + res.on('searchEntry', entry => { + Logger.debug('adding entry...') + items.push(entry.object) + }) + Logger.debug('Attaching to error events...') + res.on('end', result => { + Logger.debug('end of entries.') + Logger.info(`Search complete, returning ${items.length} items.`) + resolve(items); + }) + + }) + }) + } + authenticateUser(username, password) { + Logger.info('Authenticating LDAP User...'); + return new Promise((resolve, reject) => { + const tempClient = ldap.createClient({ + url: global.ldapUrl, + bindDN: username, + bindCredentials: password + }) + let onError, onConnect, onTimeout; + onError = err => { + Logger.error('Error Connecting.') + tempClient.removeListener('connect', onConnect); + resolve(false); + }; + onConnect = () => { + Logger.debug('Connected.') + Logger.debug('Binding...') + tempClient.removeListener('error', onError); + tempClient.bind(username, password, err => { + if (err) { + Logger.error('Error Binding.') + resolve(false); + } + else { + Logger.debug('Bound.') + tempClient.unbind(err => { + if (err) { + Logger.error('Error Unbinding.') + resolve(false); + } else { + Logger.info('User Authenticated.') + resolve(true); + } + }) + } + // client.search + }) + }; + onTimeout = () => { + Logger.error('Timeout Connecting.') + resolve(false); + } + tempClient.once('connect', onConnect) + tempClient.once('error', onError) + tempClient.once('connectTimeout', onTimeout) + }); + } + +} +module.exports = new LdapAuth() \ No newline at end of file