adauth
Version:
Authenticate against an Active Directory domain via LDAP
457 lines (456 loc) • 25.1 kB
JavaScript
"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;