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
JavaScript
"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;