UNPKG

node-expose-sspi-strict

Version:

Expose the Microsoft Windows SSPI interface in order to do NTLM and Kerberos authentication.

356 lines (355 loc) 16.7 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; 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 __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; exports.__esModule = true; exports.Client = exports.getSPNFromURI = void 0; var node_fetch_1 = __importDefault(require("node-fetch")); var dns = __importStar(require("dns")); var api_1 = require("../../lib/api"); var base64_arraybuffer_1 = require("base64-arraybuffer"); var debug_1 = __importDefault(require("debug")); var debug = debug_1["default"]('node-expose-sspi:client'); // Thanks to : // - // - /** * Get the SPN the same way Chrome/Firefox or IE does. * * Links: * - getting the domain name: https://stackoverflow.com/questions/8498592/extract-hostname-name-from-string * - algo of IE : https://support.microsoft.com/en-us/help/4551934/kerberos-failures-in-internet-explorer * * @param {string} url * @returns {string} */ function getSPNFromURI(url) { return __awaiter(this, void 0, void 0, function () { var msDomainName, matches, urlDomain, urlFQDN, hostname, records, e_1, result; return __generator(this, function (_a) { switch (_a.label) { case 0: msDomainName = api_1.sysinfo.GetComputerNameEx('ComputerNameDnsDomain'); if (msDomainName.length === 0) { debug('Client running on a host that is not part of a Microsoft domain'); return [2 /*return*/, 'whatever']; } matches = /^https?\:\/\/([^\/:?#]+)(?:[\/:?#]|$)/i.exec(url); urlDomain = matches && matches[1]; if (!urlDomain) { throw new Error('url is not well parsed. url=' + url); } debug('urlDomain: ', urlDomain); if (['localhost', '127.0.0.1'].includes(urlDomain)) { return [2 /*return*/, 'HTTP/localhost']; } urlFQDN = urlDomain.includes('.') ? urlDomain : urlDomain + '.' + msDomainName; hostname = urlFQDN; _a.label = 1; case 1: _a.trys.push([1, 5, , 6]); _a.label = 2; case 2: if (!true) return [3 /*break*/, 4]; return [4 /*yield*/, dns.promises.resolve(hostname, 'CNAME')]; case 3: records = _a.sent(); debug('records', records); if (records.length === 0) { return [3 /*break*/, 4]; } hostname = records[0]; return [3 /*break*/, 2]; case 4: return [3 /*break*/, 6]; case 5: e_1 = _a.sent(); debug('DNS error', e_1); return [3 /*break*/, 6]; case 6: result = 'HTTP/' + hostname; debug('result: ', result); return [2 /*return*/, result]; } }); }); } exports.getSPNFromURI = getSPNFromURI; /** * Allow to fetch url with a system that uses the negotiate protocol. * Cookies are managed if necessary during the process. * * @export * @class Client */ var Client = /** @class */ (function () { function Client() { this.cookieList = {}; this.ssp = 'Negotiate'; } Client.prototype.saveCookies = function (response) { var _this = this; response.headers.forEach(function (value, name) { if (name !== 'Set-Cookie'.toLowerCase()) { return; } // parse something like <key>=<val>[; Expires=xxxxx;] var _a = value.split(/[=;]/g), key = _a[0], val = _a[1]; debug('val: ', val); debug('key: ', key); _this.cookieList[key] = val; }); debug('cookieList: ', this.cookieList); }; Client.prototype.restituteCookies = function (requestInit) { var _this = this; var cookieStr = Object.keys(this.cookieList) .map(function (key) { return key + '=' + _this.cookieList[key]; }) .join('; '); if (cookieStr.length === 0) { return; } Object.assign(requestInit.headers, { cookie: cookieStr }); }; /** * Set the credentials for running the client as another user. * * By default, the credentials are the logged windows account. * * @param {string} domain * @param {string} user * @param {string} password * @memberof Client */ Client.prototype.setCredentials = function (domain, user, password) { this.domain = domain; this.user = user; this.password = password; }; /** * Force the targetName to a value. * * For Kerberos, the targetName is the SPN (Service Principal Name). * * @param {string} targetName * @memberof Client */ Client.prototype.setTargetName = function (targetName) { this.targetName = targetName; }; /** * Set the Security Support Provider (NTLM, Kerberos, Negotiate) * * @param {SecuritySupportProvider} ssp * @memberof Client */ Client.prototype.setSSP = function (ssp) { this.ssp = ssp; }; /** * Works as the fetch function of node-fetch node module. * This function can handle the negotiate protocol with SPNEGO tokens. * * @param {string} resource - the URL to fetch * @param {RequestInit} [init] - the options (headers, body, etc.) * @returns {Promise<Response>} a promise with the HTTP response. * @memberof Client */ Client.prototype.fetch = function (resource, init) { return __awaiter(this, void 0, void 0, function () { var response, result; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, node_fetch_1["default"](resource, init)]; case 1: response = _a.sent(); return [4 /*yield*/, this.handleAuth(response, resource, init)]; case 2: result = _a.sent(); return [2 /*return*/, result]; } }); }); }; /** * The authentication negotiate protocol is handled by this function. * It is called by `Client.fetch`. * * @private * @param {Response} response * @param {string} resource * @param {RequestInit} [init={}] * @returns {Promise<Response>} * @memberof Client */ Client.prototype.handleAuth = function (response, resource, init) { if (init === void 0) { init = {}; } return __awaiter(this, void 0, void 0, function () { var credInput, clientCred, packageInfo, targetName, _a, input, clientSecurityContext, base64, requestInit, buffer; return __generator(this, function (_b) { switch (_b.label) { case 0: debug('start response.headers', response.headers); // has cookies ? this.saveCookies(response); if (!response.headers.has('www-authenticate')) { debug('no header www-authenticate'); return [2 /*return*/, response]; } if (response && !response.headers.get('www-authenticate').startsWith('Negotiate')) { debug('no header www-authenticate with Negotiate:', response.headers.get('www-authenticate')); return [2 /*return*/, response]; } if (response.status !== 401) { debug('no status 401'); return [2 /*return*/, response]; } debug('starting negotiate auth'); credInput = { packageName: this.ssp, credentialUse: 'SECPKG_CRED_OUTBOUND' }; if (this.user) { credInput.authData = { domain: this.domain, user: this.user, password: this.password }; } clientCred = api_1.sspi.AcquireCredentialsHandle(credInput); packageInfo = api_1.sspi.QuerySecurityPackageInfo(this.ssp); _a = this.targetName; if (_a) return [3 /*break*/, 2]; return [4 /*yield*/, getSPNFromURI(resource)]; case 1: _a = (_b.sent()); _b.label = 2; case 2: targetName = _a; input = { credential: clientCred.credential, targetName: targetName, cbMaxToken: packageInfo.cbMaxToken, targetDataRep: 'SECURITY_NATIVE_DREP' }; debug('input: ', input); clientSecurityContext = api_1.sspi.InitializeSecurityContext(input); base64 = base64_arraybuffer_1.encode(clientSecurityContext.SecBufferDesc.buffers[0]); requestInit = __assign({}, init); requestInit.headers = __assign(__assign({}, init.headers), { Authorization: 'Negotiate ' + base64 }); // cookies case this.restituteCookies(requestInit); debug('first requestInit.headers', requestInit.headers); return [4 /*yield*/, node_fetch_1["default"](resource, requestInit)]; case 3: response = _b.sent(); debug('first response.headers', response.headers); this.saveCookies(response); _b.label = 4; case 4: if (!(response.headers.has('www-authenticate') && response.status === 401 && response.headers.get('www-authenticate').startsWith('Negotiate '))) return [3 /*break*/, 6]; buffer = base64_arraybuffer_1.decode(response.headers.get('www-authenticate').substring('Negotiate '.length)); input = { credential: clientCred.credential, targetName: targetName, cbMaxToken: packageInfo.cbMaxToken, serverSecurityContext: { SecBufferDesc: { ulVersion: 0, buffers: [buffer] } }, contextHandle: clientSecurityContext.contextHandle, targetDataRep: 'SECURITY_NATIVE_DREP' }; clientSecurityContext = api_1.sspi.InitializeSecurityContext(input); base64 = base64_arraybuffer_1.encode(clientSecurityContext.SecBufferDesc.buffers[0]); requestInit = __assign({}, init); requestInit.headers = __assign(__assign({}, init.headers), { Authorization: 'Negotiate ' + base64 }); this.restituteCookies(requestInit); debug('other requestInit.headers', requestInit.headers); return [4 /*yield*/, node_fetch_1["default"](resource, requestInit)]; case 5: response = _b.sent(); debug('other response.headers', response.headers); this.saveCookies(response); return [3 /*break*/, 4]; case 6: debug('handleAuth: end'); return [2 /*return*/, response]; } }); }); }; return Client; }()); exports.Client = Client;