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'; type MechanismProperties = { gssapiCanonicalizeHostName?: boolean; SERVICE_NAME?: string; SERVICE_REALM?: string; }; export class GSSAPI extends AuthProvider { 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): 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 }); } let spn = `${serviceName}${process.platform === 'win32' ? '/' : '@'}${host}`; 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 ): 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 ): 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); }); }); } function performGssapiCanonicalizeHostName( host: string, mechanismProperties: MechanismProperties, callback: Callback ): void { if (!mechanismProperties.gssapiCanonicalizeHostName) return callback(undefined, host); // Attempt to resolve the host name dns.resolveCname(host, (err, r) => { if (err) return callback(err); // Get the first resolve host id if (Array.isArray(r) && r.length > 0) { return callback(undefined, r[0]); } callback(undefined, host); }); }