UNPKG

node-expose-sspi-strict

Version:

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

190 lines (173 loc) 6.3 kB
var createError = require('http-errors') import { decode, encode } from 'base64-arraybuffer'; import { hexDump, getMessageType } from './misc'; import { sspi, AcceptSecurityContextInput } from '../../lib/api'; import { SSO } from './SSO'; import { ServerContextHandleManager } from './ServerContextHandleManager'; import dbg from 'debug'; import { AuthOptions, Middleware, NextFunction } from './interfaces'; import { IncomingMessage, ServerResponse } from 'http'; const debug = dbg('node-expose-sspi:auth'); /** * Tries to get SSO information from browser. If success, the SSO info * is stored under req.sso * * @export * @param {AuthOptions} [options={}] * @returns {RequestHandler} */ export function auth(options: AuthOptions = {}): Middleware { const opts: AuthOptions = { useActiveDirectory: true, useGroups: true, useOwner: false, useCookies: true, groupFilterRegex: '.*', allowsGuest: false, allowsAnonymousLogon: false, }; Object.assign(opts, options); let { credential, tsExpiry } = sspi.AcquireCredentialsHandle({ packageName: 'Negotiate', }); const checkCredentials = (): void => { if (tsExpiry < new Date()) { // renew server credentials sspi.FreeCredentialsHandle(credential); const renewed = sspi.AcquireCredentialsHandle({ packageName: 'Negotiate', }); credential = renewed.credential; tsExpiry = renewed.tsExpiry; } }; const schManager = new ServerContextHandleManager(10000); // returns the node middleware. return ( req: IncomingMessage, res: ServerResponse, next: NextFunction ): void => { (async (): Promise<void> => { try { const authorization = req.headers.authorization; if (!authorization) { debug('no authorization key in header'); res.statusCode = 401; res.setHeader('WWW-Authenticate', 'Negotiate'); return res.end(); } if (!authorization.startsWith('Negotiate ')) { res.statusCode = 400; return res.end(`Malformed authentication token: ${authorization}`); } checkCredentials(); const cookieToken = opts.useCookies ? schManager.initCookie(req, res) : undefined; debug('cookieToken: ', cookieToken); const token = authorization.substring('Negotiate '.length); const messageType = getMessageType(token); debug('messageType: ', messageType); const buffer = decode(token); debug(hexDump(buffer)); // test if first token if ( messageType === 'NTLM_NEGOTIATE_01' || messageType === 'Kerberos_1' ) { await schManager.waitForReleased(cookieToken!); debug('schManager waitForReleased finished.'); const ssoMethod = messageType.startsWith('NTLM') ? 'NTLM' : 'Kerberos'; schManager.setMethod(ssoMethod, cookieToken!); } const input: AcceptSecurityContextInput = { credential, clientSecurityContext: { SecBufferDesc: { ulVersion: 0, buffers: [buffer], }, }, }; const serverContextHandle = schManager.getServerContextHandle( cookieToken! ); if (serverContextHandle) { debug('adding to input a serverContextHandle (not first exchange)'); input.contextHandle = serverContextHandle; } debug('input just before calling AcceptSecurityContext', input); const serverSecurityContext = sspi.AcceptSecurityContext(input); debug( 'serverSecurityContext just after AcceptSecurityContext', serverSecurityContext ); if ( !['SEC_E_OK', 'SEC_I_CONTINUE_NEEDED'].includes( serverSecurityContext.SECURITY_STATUS! ) ) { // 'SEC_I_COMPLETE_AND_CONTINUE', 'SEC_I_COMPLETE_NEEDED' are considered as errors because it is used // only by 'Digest' SSP. (not by Negotiate, Kerberos or NTLM) if (serverSecurityContext.SECURITY_STATUS === 'SEC_E_LOGON_DENIED') { res.statusCode = 401; return res.end( `SEC_E_LOGON_DENIED. (incorrect login/password, or account disabled, or locked, etc.). Protocol Message = ${messageType}.` ); } throw new Error( 'AcceptSecurityContext error: ' + serverSecurityContext.SECURITY_STATUS ); } schManager.set(serverSecurityContext.contextHandle!, cookieToken!); debug('AcceptSecurityContext output buffer'); debug(hexDump(serverSecurityContext.SecBufferDesc.buffers[0])); if (serverSecurityContext.SECURITY_STATUS === 'SEC_I_CONTINUE_NEEDED') { res.statusCode = 401; res.setHeader( 'WWW-Authenticate', 'Negotiate ' + encode(serverSecurityContext.SecBufferDesc.buffers[0]) ); return res.end(); } const lastServerContextHandle = schManager.getServerContextHandle( cookieToken! ); const method = schManager.getMethod(cookieToken!); const sso = new SSO(lastServerContextHandle, method); sso.setOptions(opts); await sso.load(); req.sso = sso.getJSON(); sspi.DeleteSecurityContext(lastServerContextHandle); schManager.release(cookieToken!); // check if user is allowed. if ( !opts.allowsAnonymousLogon && req.sso.user.name === 'ANONYMOUS LOGON' ) { res.statusCode = 401; return res.end('Anonymous login not authorized.'); } if (!opts.allowsGuest && req.sso.user.name === 'Guest') { res.statusCode = 401; return res.end('Guest not authorized.'); } // user authenticated and allowed. res.setHeader( 'WWW-Authenticate', 'Negotiate ' + encode(serverSecurityContext.SecBufferDesc.buffers[0]) ); return next(); } catch (e) { schManager.release(); console.error(e); next(createError(401, `Error while doing SSO: ${e.message}`)); } })(); }; }