UNPKG

adauth

Version:

Authenticate against an Active Directory domain via LDAP

457 lines (456 loc) 25.1 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var _ADAuth_instances, _ADAuth_log, _ADAuth_userCache, _ADAuth_salt, _ADAuth_userClient, _ADAuth_getGroups, _ADAuth_initialised, _ADAuth_emitDebugCall, _ADAuth_emitDebugSyncCall, _ADAuth_emitDebugEvent, _ADAuth_logTrace, _ADAuth_handleError, _ADAuth_assertInitialised, _ADAuth_search, _ADAuth_findUser, _ADAuth_findObjectBySID, _ADAuth_findGroups; Object.defineProperty(exports, "__esModule", { value: true }); const assert_1 = __importDefault(require("assert")); const events_1 = require("events"); const fs_1 = __importDefault(require("fs")); const make_fetch_happen_1 = __importDefault(require("make-fetch-happen")); const ldapjs_1 = __importDefault(require("ldapjs")); const bcryptjs_1 = __importDefault(require("bcryptjs")); const valid_url_1 = __importDefault(require("valid-url")); const cache_1 = __importDefault(require("./cache")); const ad_utils_1 = require("./ad-utils"); const getSubset = (obj, ...keys) => { return keys.reduce((acc, key) => { if (Object.prototype.hasOwnProperty.call(obj, key)) { acc[key] = obj[key]; } return acc; }, {}); }; const wash = (obj) => JSON.parse(JSON.stringify(obj)); class ADAuth extends events_1.EventEmitter { constructor(init) { super(); _ADAuth_instances.add(this); _ADAuth_log.set(this, void 0); _ADAuth_userCache.set(this, void 0); _ADAuth_salt.set(this, void 0); _ADAuth_userClient.set(this, void 0); _ADAuth_getGroups.set(this, void 0); _ADAuth_initialised.set(this, false); assert_1.default.ok(init, 'Options not provided'); assert_1.default.ok(init.url, 'AD domain controller LDAP URL not defined (options.url)'); assert_1.default.ok(init.domainDN, 'Domain DN not defined (options.domainDN)'); const options = Object.assign({ searchBase: init.domainDN, groupSearchBase: init.domainDN, searchFilterByDN: '(&(objectCategory=user)(objectClass=user)(distinguishedName={{dn}}))', searchFilterByUPN: '(&(objectCategory=user)(objectClass=user)(userPrincipalName={{upn}}))', searchFilterBySAN: '(&(objectCategory=user)(objectClass=user)(samAccountName={{username}}))', groupSearchFilter: '(&(objectCategory=group)(objectClass=group)(member={{dn}}))', searchScope: 'sub', bindProperty: 'dn', groupSearchScope: 'sub', groupDNProperty: 'dn', }, init); this.options = options; __classPrivateFieldSet(this, _ADAuth_log, options.log && options.log.child({ component: 'adauth' }, true), "f"); if (options.cache) { __classPrivateFieldSet(this, _ADAuth_userCache, new cache_1.default(100, 300, __classPrivateFieldGet(this, _ADAuth_log, "f"), 'user'), "f"); } this.clientOptions = getSubset(options, 'url', 'tlsOptions', 'socketPath', 'log', 'timeout', 'connectTimeout', 'idleTimeout', 'strictDN', 'queueSize', 'queueTimeout', 'queueDisable'); if (options.groupSearchBase && options.groupSearchFilter) { if (typeof options.groupSearchFilter === 'string') { const { groupSearchFilter } = options; options.groupSearchFilter = user => groupSearchFilter .replace(/{{dn}}/g, user[options.groupDNProperty]) .replace(/{{username}}/g, user.uid); } __classPrivateFieldSet(this, _ADAuth_getGroups, (user, opts) => __awaiter(this, void 0, void 0, function* () { return yield __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_findGroups).call(this, user, opts); }), "f"); } else { __classPrivateFieldSet(this, _ADAuth_getGroups, (user) => __awaiter(this, void 0, void 0, function* () { return user; }), "f"); } } initialise() { return __awaiter(this, void 0, void 0, function* () { if (__classPrivateFieldGet(this, _ADAuth_userCache, "f")) { __classPrivateFieldSet(this, _ADAuth_salt, yield bcryptjs_1.default.genSalt(), "f"); } const { tlsOptions, starttls, debug } = this.options; if (typeof (tlsOptions === null || tlsOptions === void 0 ? void 0 : tlsOptions.ca) === 'string') { const { ca } = tlsOptions; try { if (valid_url_1.default.isWebUri(ca)) { const response = yield make_fetch_happen_1.default(ca, { method: 'GET' }); if (!response.ok) { throw new Error(`Failure getting CA certificate: received response code ${response.status}`); } tlsOptions.ca = yield response.buffer(); } else { tlsOptions.ca = fs_1.default.readFileSync(ca); } } catch (error) { __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_handleError).call(this, error); } } __classPrivateFieldSet(this, _ADAuth_userClient, ldapjs_1.default.createClient(this.clientOptions), "f"); __classPrivateFieldGet(this, _ADAuth_userClient, "f").on('error', error => { debug && __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_emitDebugEvent).call(this, 'error', { error: wash(error) }); __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_handleError).call(this, error); }); if (starttls) { __classPrivateFieldGet(this, _ADAuth_userClient, "f").starttls(tlsOptions, undefined, error => error && __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_handleError).call(this, error)); } __classPrivateFieldGet(this, _ADAuth_userClient, "f").on('connectTimeout', error => { debug && __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_emitDebugEvent).call(this, 'connectTimeout', { error: wash(error) }); __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_handleError).call(this, error); }); __classPrivateFieldSet(this, _ADAuth_initialised, true, "f"); }); } static create(init) { return __awaiter(this, void 0, void 0, function* () { const auth = new ADAuth(init); yield auth.initialise(); return auth; }); } authenticate(username, password) { return __awaiter(this, void 0, void 0, function* () { __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_assertInitialised).call(this); if (!password) { throw new Error('No password provided'); } if (__classPrivateFieldGet(this, _ADAuth_userCache, "f")) { const cached = __classPrivateFieldGet(this, _ADAuth_userCache, "f").get(username); if (cached && (yield bcryptjs_1.default.compare(password, cached.password))) { return cached.user; } } const { debug } = this.options; try { yield new Promise((resolve, reject) => { __classPrivateFieldGet(this, _ADAuth_userClient, "f").bind(username, password, error => { debug && __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_emitDebugCall).call(this, 'bind', { args: [username, password], error: error && wash(error), }); if (error) { return reject(error); } resolve(); }); }); } catch (error) { __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_logTrace).call(this, 'AD authenticate: bind error: %s', error); throw error; } const user = yield __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_findUser).call(this, username); if (!user) { throw new Error(`No such user: "${username}"`); } let groups; try { groups = yield __classPrivateFieldGet(this, _ADAuth_getGroups, "f").call(this, user); } catch (error) { __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_logTrace).call(this, 'AD authenticate: group search error: %s', error); throw error; } const userWithGroups = Object.assign({}, user, { groups }); if (__classPrivateFieldGet(this, _ADAuth_userCache, "f")) { try { const hash = yield bcryptjs_1.default.hash(password, __classPrivateFieldGet(this, _ADAuth_salt, "f")); __classPrivateFieldGet(this, _ADAuth_userCache, "f").set(username, { password: hash, user: userWithGroups, }); } catch (error) { __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_logTrace).call(this, 'AD authenticate: bcrypt error, not caching %s', error); } } return userWithGroups; }); } close() { return __awaiter(this, void 0, void 0, function* () { __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_assertInitialised).call(this); const { debug } = this.options; yield new Promise(resolve => { __classPrivateFieldGet(this, _ADAuth_userClient, "f").unbind(error => { debug && __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_emitDebugCall).call(this, 'unbind', { error: error && wash(error), }); resolve(); }); }); }); } dispose() { return __awaiter(this, void 0, void 0, function* () { if (__classPrivateFieldGet(this, _ADAuth_initialised, "f")) { const { debug } = this.options; yield this.close(); __classPrivateFieldSet(this, _ADAuth_initialised, false, "f"); __classPrivateFieldGet(this, _ADAuth_userClient, "f").destroy(); debug && __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_emitDebugSyncCall).call(this, 'destroy'); __classPrivateFieldSet(this, _ADAuth_userClient, undefined, "f"); } }); } } _ADAuth_log = new WeakMap(), _ADAuth_userCache = new WeakMap(), _ADAuth_salt = new WeakMap(), _ADAuth_userClient = new WeakMap(), _ADAuth_getGroups = new WeakMap(), _ADAuth_initialised = new WeakMap(), _ADAuth_instances = new WeakSet(), _ADAuth_emitDebugCall = function _ADAuth_emitDebugCall(func, { args = [], error, resolve, } = {}) { this.emit('debug', { call: func, arguments: args, error, resolve, }); }, _ADAuth_emitDebugSyncCall = function _ADAuth_emitDebugSyncCall(func, { args = [], error, return: returnValue, } = {}) { this.emit('debug', { call: func, arguments: args, error, return: returnValue, }); }, _ADAuth_emitDebugEvent = function _ADAuth_emitDebugEvent(event, data) { this.emit('debug', { event, data, }); }, _ADAuth_logTrace = function _ADAuth_logTrace(formatString, ...args) { if (__classPrivateFieldGet(this, _ADAuth_log, "f")) { __classPrivateFieldGet(this, _ADAuth_log, "f").trace(formatString, ...args); } }, _ADAuth_handleError = function _ADAuth_handleError(error) { __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_logTrace).call(this, 'LDAP-emitted error: %s', error); this.emit('error', error); }, _ADAuth_assertInitialised = function _ADAuth_assertInitialised() { assert_1.default.ok(__classPrivateFieldGet(this, _ADAuth_initialised, "f"), 'ADAuth instance is not initialised. Please call initialise() first.'); }, _ADAuth_search = function _ADAuth_search(searchBase, searchOptions = {}) { return __awaiter(this, void 0, void 0, function* () { __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_assertInitialised).call(this); const { includeRaw, debug } = this.options; return yield new Promise((resolve, reject) => { let debugOptions; if (debug) { debugOptions = wash(searchOptions); } __classPrivateFieldGet(this, _ADAuth_userClient, "f").search(searchBase, searchOptions, (error, searchResult) => { debug && __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_emitDebugCall).call(this, 'search', { args: [searchBase, debugOptions], error: error && wash(error), resolve: wash(searchResult), }); if (error) { return reject(error); } const items = []; searchResult.on('searchEntry', entry => { debug && __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_emitDebugEvent).call(this, 'searchResult.searchEntry', { searchArgs: [searchBase, debugOptions], entry: Object.assign(wash(entry), { object: wash(entry.object), raw: wash(entry.raw), attributes: wash(entry.attributes), }), }); items.push(Object.assign(Object.assign({}, entry.object), { objectSid: entry.raw.objectSid ? ad_utils_1.binarySIDToString(entry.raw.objectSid) : undefined, objectGUID: entry.raw.objectGUID ? ad_utils_1.binaryGUIDToString(entry.raw.objectGUID) : undefined, _raw: includeRaw ? entry.raw : undefined })); }); searchResult.on('error', searchError => { debug && __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_emitDebugEvent).call(this, 'searchResult.error', { searchArgs: [searchBase, debugOptions], error: wash(searchError), }); reject(searchError); }); searchResult.on('end', result => { debug && __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_emitDebugEvent).call(this, 'searchResult.end', { searchArgs: [searchBase, debugOptions], result: wash(result), }); if (result.status !== 0) { return reject(new Error(`Non-zero status from LDAP search: ${result.status}`)); } return resolve(items); }); }); }); }); }, _ADAuth_findUser = function _ADAuth_findUser(username, options = {}) { return __awaiter(this, void 0, void 0, function* () { __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_assertInitialised).call(this); if (!username) { throw new Error('empty username'); } const { domainDN, searchBase, searchAttributes, searchFilterBySAN, searchFilterByUPN, searchScope, } = this.options; let searchFilter; if (username.includes('\\')) { const splitUsername = username.split('\\'); const netBIOSDomainName = splitUsername[0]; const samAccountName = splitUsername[1]; const domains = yield __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_search).call(this, domainDN, { filter: `(distinguishedName=${domainDN})`, attributes: ['msDS-PrincipalName'], scope: searchScope, }); let inName = netBIOSDomainName.toUpperCase(); if (inName.charAt(inName.length - 1) === '\\') { inName = inName.substring(0, inName.length - 1); } let fdName = domains[0]['msDS-PrincipalName'].toUpperCase(); if (fdName.charAt(fdName.length - 1) === '\\') { fdName = fdName.substring(0, fdName.length - 1); } if (inName === fdName) { searchFilter = searchFilterBySAN.replace(/{{username}}/g, ad_utils_1.escapeADString(samAccountName)); } else { const errorMsg = `cannot find known domain with netBIOS domain name ${netBIOSDomainName}`; __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_logTrace).call(this, errorMsg); throw new Error(errorMsg); } } else if (username.includes('@')) { searchFilter = searchFilterByUPN.replace(/{{upn}}/g, ad_utils_1.escapeADString(username)); } else { searchFilter = searchFilterBySAN.replace(/{{username}}/g, ad_utils_1.escapeADString(username)); } let result; try { result = yield __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_search).call(this, searchBase, Object.assign(Object.assign({}, options), { filter: searchFilter, scope: searchScope, attributes: searchAttributes || undefined })); } catch (error) { __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_logTrace).call(this, 'AD authenticate: user search error: %s %s %s', error.code, error.name, error.message); throw error; } switch (result.length) { case 0: return; case 1: return result[0]; default: throw new Error(`Unexpected number of matches (${result.length}) for username "${username}"`); } }); }, _ADAuth_findObjectBySID = function _ADAuth_findObjectBySID(sid, options = {}) { return __awaiter(this, void 0, void 0, function* () { __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_assertInitialised).call(this); const sidString = typeof sid !== 'string' && Array.isArray(sid) ? ad_utils_1.binarySIDToString(sid) : sid; const baseDN = options.baseDN || this.options.domainDN; const { groupSearchScope } = this.options; try { const result = yield __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_search).call(this, baseDN, Object.assign(Object.assign({}, options), { filter: `(objectSid=${sidString})`, scope: groupSearchScope })); return result && result[0]; } catch (error) { __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_logTrace).call(this, 'Find object by SID: search error: %s %s %s', error.code, error.name, error.message); } }); }, _ADAuth_findGroups = function _ADAuth_findGroups(user, options = {}) { return __awaiter(this, void 0, void 0, function* () { __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_assertInitialised).call(this); if (!user) { throw new Error('No user'); } const objGroups = []; const resolved = new Set(); const { groupSearchFilter, groupSearchScope, groupDNProperty, groupSearchBase, groupSearchAttributes, } = this.options; const resolveGroups = (obj) => __awaiter(this, void 0, void 0, function* () { if (!obj) { throw new Error('AD authenticate: Cannot find groups for undefined object'); } const searchFilter = typeof groupSearchFilter === 'string' ? groupSearchFilter.replace(/{{dn}}/g, obj[groupDNProperty]) : groupSearchFilter(obj); const searchOptions = Object.assign(Object.assign({}, options), { filter: searchFilter, scope: groupSearchScope, attributes: groupSearchAttributes }); let groups; try { groups = yield __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_search).call(this, groupSearchBase, searchOptions); } catch (error) { __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_logTrace).call(this, 'AD authenticate: group search error: %s %s %s', error.code, error.name, error.message); throw error; } if (obj.primaryGroupID) { const primaryGroupSID = obj.objectSid.substring(0, obj.objectSid.lastIndexOf('-') + 1) + obj.primaryGroupID; let primaryGroup; try { primaryGroup = yield __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_findObjectBySID).call(this, primaryGroupSID, searchOptions); } catch (error) { __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_logTrace).call(this, 'Find primary group by SID: search error: %s %s %s', error.code, error.name, error.message); throw error; } if (primaryGroup) { groups.unshift(primaryGroup); if (Array.isArray(obj.memberOf)) { obj.memberOf.unshift(primaryGroup.dn); } else if (obj.memberOf) { obj.memberOf = [primaryGroup.dn, obj.memberOf]; } } } const needResolution = []; for (const group of groups) { if (!resolved.has(group.objectSid)) { resolved.add(group.objectSid); needResolution.push(group); objGroups.push(group); } } yield Promise.all(needResolution.map(group => () => __awaiter(this, void 0, void 0, function* () { try { yield resolveGroups(group); } catch (error) { __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_handleError).call(this, error); throw error; } }))); }); try { yield resolveGroups(user); } catch (error) { __classPrivateFieldGet(this, _ADAuth_instances, "m", _ADAuth_logTrace).call(this, 'AD authenticate: group search error: %s %s %s', error.code, error.name, error.message); throw error; } return objGroups; }); }; exports.default = ADAuth;