win-sso
Version:
NTLM single-sign-on for Node.js. Only Windows OS supported.
193 lines (179 loc) • 6.65 kB
text/typescript
import { debug } from "./utils/debug.logger";
import { PeerCertificate } from "tls";
import path from "path";
import { IWinSsoAddon } from "./utils/i.win-sso-addon";
let winSsoAddon: IWinSsoAddon;
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
winSsoAddon = require("node-gyp-build")(path.join(__dirname, ".."));
debug("Loaded win-sso native module");
} catch {
debug("Could not load win-sso native module");
}
/**
* Creates authentication tokens for NTLM or Negotiate handshake using the executing users credentials.
*/
export class WinSso {
private static NEGOTIATE_NTLM2_KEY = 1 << 19;
private authContextId: number;
private securityPackage: string;
/**
* Creates an authentication context for SSO.
* This allocates memory buffers, the freeAuthContext method should be called
* to free them (on error or after authentication is no longer needed)
* @param securityPackage The name of the security package (NTLM or Negotiate)
* @param targetHost The FQDN hostname of the target (optional for NTLM, required for Kerberos)
* @param peerCert The certificate of the target server
* (optional, for HTTPS channel binding)
* @param flags Flags to set in the authentication context
* If not set, NTML defaults to no flags, while Negotiate defaults to ISC_REQ_MUTUAL_AUTH | ISC_REQ_SEQUENCE_DETECT
* (optional, allows customizing security features)
*/
constructor(
securityPackage: string,
targetHost: string | undefined,
peerCert: PeerCertificate | undefined,
flags: number | undefined
) {
this.securityPackage = securityPackage;
let applicationData: Buffer;
if (!targetHost) {
targetHost = "";
}
if (peerCert) {
applicationData = this.getChannelBindingsApplicationData(peerCert);
} else {
applicationData = Buffer.alloc(0);
}
this.authContextId = winSsoAddon.createAuthContext(
securityPackage,
targetHost,
applicationData,
flags
);
}
/**
* Retrieves the username of the logged in user
* @returns user name including domain
*/
static getLogonUserName(): string {
return winSsoAddon.getLogonUserName();
}
/**
* Transforms target TLS certificate into a channel binding application data buffer
* @param peerCert Target TLS certificate
* @returns Application data buffer
*/
private getChannelBindingsApplicationData(peerCert: PeerCertificate): Buffer {
const hash = peerCert.fingerprint256.replace(/:/g, "");
const hashBuf = Buffer.from(hash, "hex");
const tlsServerEndPoint = "tls-server-end-point:";
const applicationDataBuffer = Buffer.alloc(
tlsServerEndPoint.length + hashBuf.length
);
applicationDataBuffer.write(tlsServerEndPoint, 0, "ascii");
hashBuf.copy(applicationDataBuffer, tlsServerEndPoint.length);
return applicationDataBuffer;
}
/**
* Releases all allocated resources for the authorization context.
* Should be called when the context is no longer required, such as when the
* socket was closed.
*/
freeAuthContext() {
winSsoAddon.freeAuthContext(this.authContextId);
}
/**
* Creates an authentication request token
* @returns Raw token buffer
*/
createAuthRequest(): Buffer {
const token = winSsoAddon.createAuthRequest(this.authContextId);
debug(
"Created " + this.securityPackage + " authentication request token",
token.toString("base64")
);
return token;
}
/**
* Creates an authentication request header
* @returns The www-authenticate header
*/
createAuthRequestHeader(): string {
const header =
this.securityPackage + " " + this.createAuthRequest().toString("base64");
return header;
}
/**
* Creates an authentication response token
* @param inTokenHeader The www-authentication header received from the target
* in response to the authentication request
* @returns Raw token buffer. May be empty if Negotiate handshake is complete.
*/
createAuthResponse(inTokenHeader: string): Buffer {
debug("Received www-authentication response", inTokenHeader);
const packageMatch = new RegExp(
"^" + this.securityPackage + "\\s([^,\\s]+)"
).exec(inTokenHeader);
if (!packageMatch) {
throw new Error(
"Invalid input token, missing " +
this.securityPackage +
" prefix: " +
inTokenHeader
);
}
const inToken = Buffer.from(packageMatch[1], "base64");
try {
const token = winSsoAddon.createAuthResponse(this.authContextId, inToken);
if (token.length > 0) {
debug(
"Created " + this.securityPackage + " authentication response token",
token.toString("base64")
);
} else {
debug("No response token, authentication complete");
}
return token;
} catch (err) {
if (
(err as Error).message ===
"Could not init security context. Result: -2146893054"
) {
// If incoming token is for NTLMv1, this error can occur when
// LMCompatibilityLevel prevents the client to send NTLMv1 messages
if (this.securityPackage === "NTLM" && this.isNtlmV1(inToken)) {
throw new Error(
"Could not create NTLM type 3 message. Incoming type 2 message uses NTLMv1, " +
"it is likely that the client is prevented from sending such messages. " +
"Update target host to use NTLMv2 (recommended) or adjust LMCompatibilityLevel on the client (insecure)"
);
}
}
throw err;
}
}
private isNtlmV1(type2message: Buffer): boolean {
if (type2message.length >= 24) {
const inTokenFlags = type2message.readInt32BE(20);
if ((inTokenFlags & WinSso.NEGOTIATE_NTLM2_KEY) === 0) {
return true;
}
}
return false;
}
/**
* Creates an authentication response header
* @param inTokenHeader The www-authentication header received from the target
* in response to the authentication request
* @returns The www-authenticate header. May be an empty string if Negotiate handshake is complete.
*/
createAuthResponseHeader(inTokenHeader: string): string {
const tokenBuffer = this.createAuthResponse(inTokenHeader);
if (tokenBuffer.length == 0) {
return "";
}
const header = this.securityPackage + " " + tokenBuffer.toString("base64");
return header;
}
}