ya-express-ntlm
Version:
120 lines (106 loc) • 5.29 kB
text/typescript
import { Request, Response, NextFunction, RequestHandler } from 'express';
import { bg, black, blue, bold, boldOff, reset, rs } from 'af-color';
import { Buffer } from 'buffer';
import { IAuthNtlmOptions, IRsn } from '../interfaces';
import { handleAuthenticate } from './handle-authenticate';
import { handleNegotiate } from './handle-negotiate';
import { debugNtlmAuthFlow, debugNtlmLdapProxyId, hnColor, hvInColor, hvOutColor } from './debug';
import { NTLMMessageParsed, NTLMMessageType, ntlmParse, NTLMType1, NTLMType2, NTLMType3 } from '../ntlm-parser';
import { prepareOptions } from '../prepare-options';
import { arrowR, Larrow } from './lib/constants';
import { transferExistingProps } from './lib/utils';
/**
* Returns data from the Authorization header: NTLM <data>
* If they can be parsed, then req.ntlm is filled
*/
const getNtlmAuthorizationData = (req: Request): string | undefined => {
const [title, data] = req.headers?.authorization?.split(' ') || [];
if (title === 'NTLM' && data) {
return data;
}
};
/**
* Fills req.ntlm with data from the Authorization header: NTLM <data>
* If they can be parsed.
*/
const fillReqNtlm = (req: Request, data: string): NTLMMessageParsed => {
const parsedData = ntlmParse(data, { compact: true }) as NTLMType1 | NTLMType2 | NTLMType3;
debugNtlmAuthFlow(`Decoded Authorization header: ${hvInColor}${JSON.stringify(parsedData, undefined, 2)}`);
['domain', 'username', 'workstation'].forEach((p) => {
if (parsedData[p]) {
req.ntlm[p] = parsedData[p];
}
});
return parsedData;
};
export const authNTLM = (authNtlmOptions?: IAuthNtlmOptions): RequestHandler => {
const options = prepareOptions(authNtlmOptions);
return async (req: Request, res: Response, next: NextFunction) => {
const rsn: IRsn = { req, res, next, options };
let userData = options.getCachedUserData(rsn);
const uri = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
const requestedURI = `${arrowR} ${req.method}: ${req.protocol}://${req.get('host')}${req.originalUrl}`;
const authorizationHeader = req.headers.authorization;
const uriA = `${requestedURI} : ${authorizationHeader ? `${hnColor}Authorization: ${hvInColor}${
authorizationHeader || ''}` : `${hnColor}No Authorization header`}`;
req.ntlm = req.ntlm || { uri };
const mTitle = `============ Start NTLM Authorization. Strategy: '${options.getStrategy(rsn)}' ==================`;
// req.ntlm.isAuthenticated must be filled in earlier when determining the presence of a session cookie
if (userData.isAuthenticated) {
if (!authorizationHeader || (authorizationHeader && req.method !== 'POST')) {
const { username, domain } = transferExistingProps({ ...userData, uri }, req.ntlm);
debugNtlmAuthFlow(`${requestedURI}\nConnection already authenticated / user: ${username} / domain: ${domain}`);
return next();
}
debugNtlmAuthFlow(`The connection is authenticated, but the "Authorization" header sent using the POST method was detected`);
}
debugNtlmAuthFlow(uriA);
if (!authorizationHeader) {
debugNtlmAuthFlow(mTitle);
debugNtlmAuthFlow(`${Larrow} Return ${blue}401${reset}: ${hnColor}WWW-Authenticate${blue}: ${hvOutColor}NTLM`);
return res
.setHeader('Content-Type', 'text/plain; charset=utf-8')
.setHeader('WWW-Authenticate', 'NTLM')
.setHeader('Date', (new Date()).toUTCString())
.status(401)
.send('401 UNAUTHORIZED');
}
// Returns data from the Authorization header: NTLM <data>
const ntlmAuthData = getNtlmAuthorizationData(req);
if (!ntlmAuthData) {
return options.handleHttpError400(res, `Authorization header does not contain NTLM data. URI ${uri}`);
}
// Fills req.ntlm with data from the Authorization header: NTLM <data>.
const { domain, messageType } = fillReqNtlm(req, ntlmAuthData);
// Domain names from NTLM messages - we believe
if (domain) {
debugNtlmLdapProxyId(`↓ ${domain}`);
req.ntlm.domain = domain;
}
const dataBuf = Buffer.from(ntlmAuthData, 'base64');
if (messageType === NTLMMessageType.UNKNOWN) {
return options.handleHttpError400(res, `Incorrect NTLM message Type ${dataBuf.readUInt8(8)}`);
}
if (messageType === NTLMMessageType.NEGOTIATE_MESSAGE) {
return handleNegotiate(rsn, dataBuf).then(() => 0);
}
if (messageType === NTLMMessageType.AUTHENTICATE_MESSAGE) {
const isNoErrors = await handleAuthenticate(rsn, dataBuf);
if (!isNoErrors) {
return; // In this case the error has already been sent over HTTP
}
userData = options.getCachedUserData(rsn);
if (!userData.isAuthenticated) {
return options.handleHttpError403(rsn);
}
if (debugNtlmAuthFlow.enabled) {
// eslint-disable-next-line no-console
console.log(`\n${bg.lGreen + black}req.ntlm:${bg.def + rs}`, userData, `\n`);
}
options.handleSuccessAuthentication(rsn);
debugNtlmAuthFlow(`${Larrow} handle success authorisation (Default ${bold + reset}next${blue}()${boldOff}${reset})`);
return;
}
return options.handleHttpError400(res, 'NTLM: Unexpected Type 2 message (CHALLENGE) in client request');
};
};