import * as dns from 'dns';
import type { Document } from '../../bson';
import { Kerberos, KerberosClient } from '../../deps';
import {
MongoError,
MongoInvalidArgumentError,
MongoMissingCredentialsError,
MongoMissingDependencyError,
MongoRuntimeError
} from '../../error';
import { Callback, ns } from '../../utils';
import { AuthContext, AuthProvider } from './auth_provider';
/** @public */
export const GSSAPICanonicalizationValue = Object.freeze({
on: true,
off: false,
none: 'none',
forward: 'forward',
forwardAndReverse: 'forwardAndReverse'
} as const);
/** @public */
export type GSSAPICanonicalizationValue =
typeof GSSAPICanonicalizationValue[keyof typeof GSSAPICanonicalizationValue];
type MechanismProperties = {
/** @deprecated use `CANONICALIZE_HOST_NAME` instead */
gssapiCanonicalizeHostName?: boolean;
CANONICALIZE_HOST_NAME?: GSSAPICanonicalizationValue;
SERVICE_HOST?: string;
SERVICE_NAME?: string;
SERVICE_REALM?: string;
};
export class GSSAPI extends AuthProvider {
override auth(authContext: AuthContext, callback: Callback): void {
const { connection, credentials } = authContext;
if (credentials == null)
return callback(
new MongoMissingCredentialsError('Credentials required for GSSAPI authentication')
);
const { username } = credentials;
function externalCommand(
command: Document,
cb: Callback<{ payload: string; conversationId: any }>
) {
return connection.command(ns('$external.$cmd'), command, undefined, cb);
}
makeKerberosClient(authContext, (err, client) => {
if (err) return callback(err);
if (client == null) return callback(new MongoMissingDependencyError('GSSAPI client missing'));
client.step('', (err, payload) => {
if (err) return callback(err);
externalCommand(saslStart(payload), (err, result) => {
if (err) return callback(err);
if (result == null) return callback();
negotiate(client, 10, result.payload, (err, payload) => {
if (err) return callback(err);
externalCommand(saslContinue(payload, result.conversationId), (err, result) => {
if (err) return callback(err);
if (result == null) return callback();
finalize(client, username, result.payload, (err, payload) => {
if (err) return callback(err);
externalCommand(
{
saslContinue: 1,
conversationId: result.conversationId,
payload
},
(err, result) => {
if (err) return callback(err);
callback(undefined, result);
}
);
});
});
});
});
});
});
}
}
function makeKerberosClient(authContext: AuthContext, callback: Callback<KerberosClient>): void {
const { hostAddress } = authContext.options;
const { credentials } = authContext;
if (!hostAddress || typeof hostAddress.host !== 'string' || !credentials) {
return callback(
new MongoInvalidArgumentError('Connection must have host and port and credentials defined.')
);
}
if ('kModuleError' in Kerberos) {
return callback(Kerberos['kModuleError']);
}
const { initializeClient } = Kerberos;
const { username, password } = credentials;
const mechanismProperties = credentials.mechanismProperties as MechanismProperties;
const serviceName = mechanismProperties.SERVICE_NAME ?? 'mongodb';
performGSSAPICanonicalizeHostName(
hostAddress.host,
mechanismProperties,
(err?: Error | MongoError, host?: string) => {
if (err) return callback(err);
const initOptions = {};
if (password != null) {
Object.assign(initOptions, { user: username, password: password });
}
const spnHost = mechanismProperties.SERVICE_HOST ?? host;
let spn = `${serviceName}${process.platform === 'win32' ? '/' : '@'}${spnHost}`;
if ('SERVICE_REALM' in mechanismProperties) {
spn = `${spn}@${mechanismProperties.SERVICE_REALM}`;
}
initializeClient(spn, initOptions, (err: string, client: KerberosClient): void => {
// TODO(NODE-3483)
if (err) return callback(new MongoRuntimeError(err));
callback(undefined, client);
});
}
);
}
function saslStart(payload?: string): Document {
return {
saslStart: 1,
mechanism: 'GSSAPI',
payload,
autoAuthorize: 1
};
}
function saslContinue(payload?: string, conversationId?: number): Document {
return {
saslContinue: 1,
conversationId,
payload
};
}
function negotiate(
client: KerberosClient,
retries: number,
payload: string,
callback: Callback<string>
): void {
client.step(payload, (err, response) => {
// Retries exhausted, raise error
if (err && retries === 0) return callback(err);
// Adjust number of retries and call step again
if (err) return negotiate(client, retries - 1, payload, callback);
// Return the payload
callback(undefined, response || '');
});
}
function finalize(
client: KerberosClient,
user: string,
payload: string,
callback: Callback<string>
): void {
// GSS Client Unwrap
client.unwrap(payload, (err, response) => {
if (err) return callback(err);
// Wrap the response
client.wrap(response || '', { user }, (err, wrapped) => {
if (err) return callback(err);
// Return the payload
callback(undefined, wrapped);
});
});
}
export function performGSSAPICanonicalizeHostName(
host: string,
mechanismProperties: MechanismProperties,
callback: Callback<string>
): void {
const mode = mechanismProperties.CANONICALIZE_HOST_NAME;
if (!mode || mode === GSSAPICanonicalizationValue.none) {
return callback(undefined, host);
}
// If forward and reverse or true
if (
mode === GSSAPICanonicalizationValue.on ||
mode === GSSAPICanonicalizationValue.forwardAndReverse
) {
// Perform the lookup of the ip address.
dns.lookup(host, (error, address) => {
// No ip found, return the error.
if (error) return callback(error);
// Perform a reverse ptr lookup on the ip address.
dns.resolvePtr(address, (err, results) => {
// This can error as ptr records may not exist for all ips. In this case
// fallback to a cname lookup as dns.lookup() does not return the
// cname.
if (err) {
return resolveCname(host, callback);
}
// If the ptr did not error but had no results, return the host.
callback(undefined, results.length > 0 ? results[0] : host);
});
});
} else {
// The case for forward is just to resolve the cname as dns.lookup()
// will not return it.
resolveCname(host, callback);
}
}
export function resolveCname(host: string, callback: Callback<string>): void {
// Attempt to resolve the host name
dns.resolveCname(host, (err, r) => {
if (err) return callback(undefined, host);
// Get the first resolve host id
if (r.length > 0) {
return callback(undefined, r[0]);
}
callback(undefined, host);
});
}