node-expose-sspi-strict
Version:
Expose the Microsoft Windows SSPI interface in order to do NTLM and Kerberos authentication.
156 lines (136 loc) • 4.6 kB
text/typescript
import { CtxtHandle } from '../../lib/api';
import * as http from 'http';
import { parseCookies } from './cookies';
import dbg from 'debug';
import { SSOMethod } from './SSO';
import { CookieToken } from './interfaces';
const debug = dbg('node-expose-sspi:schManager');
type IPromiseFn = (value?: unknown) => void;
interface AuthItem {
resolve: IPromiseFn;
reject: IPromiseFn;
timeout?: NodeJS.Timeout;
}
interface ContextInfo {
serverContextHandle?: CtxtHandle;
method?: SSOMethod;
}
const COOKIE_KEY = 'NEGOTIATE_ID';
const COOKIE_PREFIX_VALUE = 'NEGOTIATE_';
export class ServerContextHandleManager {
private serverContextHandle?: CtxtHandle;
private queue: AuthItem[] = [];
private authItem?: AuthItem;
private sessionMap = new Map<string, ContextInfo>();
private method: SSOMethod;
constructor(private delayMax = 20000) {}
initCookie(req: http.IncomingMessage, res: http.ServerResponse): CookieToken {
debug('initCookie');
let cookieToken = parseCookies(req)[COOKIE_KEY];
if (!cookieToken) {
cookieToken = COOKIE_PREFIX_VALUE + Math.floor(1e10 * Math.random());
// create a session cookie (without expiration specified)
res.setHeader('Set-Cookie', COOKIE_KEY + '=' + cookieToken);
}
if (!this.sessionMap.has(cookieToken)) {
this.sessionMap.set(cookieToken, {
method: undefined,
serverContextHandle: undefined,
} as ContextInfo);
}
return cookieToken;
}
waitForReleased(cookieToken: CookieToken): Promise<void> {
if (cookieToken) {
return Promise.resolve();
}
debug('waitForReleased: start');
return new Promise((resolve, reject) => {
debug('waitForReleased: start promise');
// if nobody else is currently authenticating then go now.
const authItem = { resolve, reject };
const timeout = setTimeout(() => {
this.tooLate(authItem);
}, this.delayMax);
if (this.authItem === undefined) {
debug(
'waitForReleased: no other authentication ongoing: we can start now.'
);
this.authItem = { resolve, reject, timeout };
return this.authItem.resolve();
}
debug(
'someone is currently authenticating, go in the queue and wait for your turn.'
);
this.queue.push({ resolve, reject, timeout });
debug('queue length', this.queue.length);
});
}
set(serverContextHandle: CtxtHandle, cookieToken: CookieToken): void {
if (cookieToken) {
const contextInfo = this.sessionMap.get(cookieToken);
contextInfo!.serverContextHandle = serverContextHandle;
return;
}
this.serverContextHandle = serverContextHandle;
}
getServerContextHandle(cookieToken: CookieToken): CtxtHandle {
if (cookieToken) {
const contextInfo = this.sessionMap.get(cookieToken);
return contextInfo!.serverContextHandle!;
}
return this.serverContextHandle!;
}
release(cookieToken?: CookieToken): void {
if (cookieToken) {
this.sessionMap.delete(cookieToken);
return;
}
if (this.authItem) {
clearTimeout(this.authItem.timeout!);
}
this.serverContextHandle = undefined;
this.authItem = undefined;
if (this.queue.length > 0) {
// it means another client B was waiting for authenticating.
// so we start authenticating this client B.
this.authItem = this.queue.shift();
debug('releasing. queue length', this.queue.length);
this.authItem!.resolve();
}
}
/**
* Used only when a negotiate connection
* does not go to its final state before timeout.
*
* Note: Do not interfer with cookies.
*
* @param {AuthItem} authItem
* @returns
* @memberof ServerContextHandleManager
*/
tooLate(authItem: AuthItem): void {
while (this.queue.length > 0) {
const ai = this.queue.shift();
clearTimeout(ai!.timeout!);
this.authItem!.reject();
}
this.authItem = authItem;
this.authItem!.resolve();
}
setMethod(method: SSOMethod, cookieToken: CookieToken): void {
if (cookieToken) {
const contextInfo = this.sessionMap.get(cookieToken);
contextInfo!.method = method;
return;
}
this.method = method;
}
getMethod(cookieToken: CookieToken): SSOMethod {
if (cookieToken) {
const contextInfo = this.sessionMap.get(cookieToken);
return contextInfo!.method!;
}
return this.method;
}
}