UNPKG

win-sso

Version:

NTLM single-sign-on for Node.js. Only Windows OS supported.

193 lines (179 loc) 6.65 kB
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; } }