Skip to content

Commit 9a115d8

Browse files
authored
refactor(kerberos): move MongoAuthProcess into driver
NODE-2730
1 parent 9bc8f64 commit 9a115d8

File tree

3 files changed

+160
-64
lines changed

3 files changed

+160
-64
lines changed

src/cmap/auth/gssapi.ts

Lines changed: 149 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
import { AuthProvider, AuthContext } from './auth_provider';
22
import { MongoError } from '../../error';
3-
import { Kerberos } from '../../deps';
3+
import { Kerberos, KerberosClient } from '../../deps';
44
import type { Callback } from '../../utils';
5+
import type { HandshakeDocument } from '../connect';
6+
import type { Document } from '../../bson';
7+
8+
const kGssapiClient = Symbol('GSSAPI_CLIENT');
9+
10+
type MechanismProperties = {
11+
gssapiCanonicalizeHostName?: boolean;
12+
};
13+
14+
import * as dns from 'dns';
515

616
export class GSSAPI extends AuthProvider {
7-
auth(authContext: AuthContext, callback: Callback): void {
17+
[kGssapiClient]: KerberosClient;
18+
prepare(
19+
handshakeDoc: HandshakeDocument,
20+
authContext: AuthContext,
21+
callback: Callback<HandshakeDocument>
22+
): void {
823
const { host, port } = authContext.options;
9-
const { connection, credentials } = authContext;
24+
const { credentials } = authContext;
1025
if (!host || !port || !credentials) {
1126
return callback(
1227
new MongoError(
@@ -21,65 +36,154 @@ export class GSSAPI extends AuthProvider {
2136
return callback(Kerberos['kModuleError']);
2237
}
2338

24-
const username = credentials.username;
25-
const password = credentials.password;
26-
const mechanismProperties = credentials.mechanismProperties;
27-
const gssapiServiceName =
39+
const { username, password, mechanismProperties } = credentials;
40+
const serviceName =
2841
mechanismProperties['gssapiservicename'] ||
2942
mechanismProperties['gssapiServiceName'] ||
3043
'mongodb';
3144

32-
const MongoAuthProcess = Kerberos.processes.MongoAuthProcess;
33-
34-
const authProcess = new MongoAuthProcess(host, port, gssapiServiceName, mechanismProperties);
45+
performGssapiCanonicalizeHostName(
46+
host,
47+
mechanismProperties as MechanismProperties,
48+
(err?: Error | MongoError, host?: string) => {
49+
if (err) return callback(err);
3550

36-
authProcess.init(username, password, err => {
37-
if (err) return callback(err, false);
38-
authProcess.transition('', (err, payload) => {
39-
if (err) return callback(err, false);
51+
const initOptions = {};
52+
if (password != null) {
53+
Object.assign(initOptions, { user: username, password: password });
54+
}
4055

41-
const command = {
42-
saslStart: 1,
43-
mechanism: 'GSSAPI',
44-
payload,
45-
autoAuthorize: 1
46-
};
56+
Kerberos.initializeClient(
57+
`${serviceName}${process.platform === 'win32' ? '/' : '@'}${host}`,
58+
initOptions,
59+
(err: string, client: KerberosClient): void => {
60+
if (err) return callback(new MongoError(err));
61+
if (client == null) return callback();
62+
this[kGssapiClient] = client;
63+
callback(undefined, handshakeDoc);
64+
}
65+
);
66+
}
67+
);
68+
}
4769

48-
connection.command('$external.$cmd', command, (err, doc) => {
49-
if (err) return callback(err, false);
70+
auth(authContext: AuthContext, callback: Callback): void {
71+
const { connection, credentials } = authContext;
72+
if (credentials == null) return callback(new MongoError('credentials required'));
73+
const { username } = credentials;
74+
const client = this[kGssapiClient];
75+
if (client == null) return callback(new MongoError('gssapi client missing'));
76+
function externalCommand(
77+
command: Document,
78+
cb: Callback<{ payload: string; conversationId: any }>
79+
) {
80+
return connection.command('$external.$cmd', command, cb);
81+
}
82+
client.step('', (err, payload) => {
83+
if (err) return callback(err);
5084

51-
authProcess.transition(doc.payload, (err, payload) => {
52-
if (err) return callback(err, false);
53-
const command = {
54-
saslContinue: 1,
55-
conversationId: doc.conversationId,
56-
payload
57-
};
85+
externalCommand(saslStart(payload), (err, result) => {
86+
if (err) return callback(err);
87+
if (result == null) return callback();
88+
negotiate(client, 10, result.payload, (err, payload) => {
89+
if (err) return callback(err);
5890

59-
connection.command('$external.$cmd', command, (err, doc) => {
60-
if (err) return callback(err, false);
91+
externalCommand(saslContinue(payload, result.conversationId), (err, result) => {
92+
if (err) return callback(err);
93+
if (result == null) return callback();
94+
finalize(client, username, result.payload, (err, payload) => {
95+
if (err) return callback(err);
6196

62-
authProcess.transition(doc.payload, (err, payload) => {
63-
if (err) return callback(err, false);
64-
const command = {
97+
externalCommand(
98+
{
6599
saslContinue: 1,
66-
conversationId: doc.conversationId,
100+
conversationId: result.conversationId,
67101
payload
68-
};
102+
},
103+
(err, result) => {
104+
if (err) return callback(err);
69105

70-
connection.command('$external.$cmd', command, (err, response) => {
71-
if (err) return callback(err, false);
72-
73-
authProcess.transition(null, err => {
74-
if (err) return callback(err);
75-
callback(undefined, response);
76-
});
77-
});
78-
});
106+
callback(undefined, result);
107+
}
108+
);
79109
});
80110
});
81111
});
82112
});
83113
});
84114
}
85115
}
116+
function saslStart(payload?: string): Document {
117+
return {
118+
saslStart: 1,
119+
mechanism: 'GSSAPI',
120+
payload,
121+
autoAuthorize: 1
122+
};
123+
}
124+
125+
function saslContinue(payload?: string, conversationId?: number): Document {
126+
return {
127+
saslContinue: 1,
128+
conversationId,
129+
payload
130+
};
131+
}
132+
133+
function negotiate(
134+
client: KerberosClient,
135+
retries: number,
136+
payload: string,
137+
callback: Callback<string>
138+
): void {
139+
client.step(payload, (err, response) => {
140+
// Retries exhausted, raise error
141+
if (err && retries === 0) return callback(err);
142+
143+
// Adjust number of retries and call step again
144+
if (err) return negotiate(client, retries - 1, payload, callback);
145+
146+
// Return the payload
147+
callback(undefined, response || '');
148+
});
149+
}
150+
151+
function finalize(
152+
client: KerberosClient,
153+
user: string,
154+
payload: string,
155+
callback: Callback<string>
156+
): void {
157+
// GSS Client Unwrap
158+
client.unwrap(payload, (err, response) => {
159+
if (err) return callback(err);
160+
161+
// Wrap the response
162+
client.wrap(response || '', { user }, (err, wrapped) => {
163+
if (err) return callback(err);
164+
165+
// Return the payload
166+
callback(undefined, wrapped);
167+
});
168+
});
169+
}
170+
171+
function performGssapiCanonicalizeHostName(
172+
host: string,
173+
mechanismProperties: MechanismProperties,
174+
callback: Callback<string>
175+
): void {
176+
if (!mechanismProperties.gssapiCanonicalizeHostName) return callback(undefined, host);
177+
178+
// Attempt to resolve the host name
179+
dns.resolveCname(host, (err, r) => {
180+
if (err) return callback(err);
181+
182+
// Get the first resolve host id
183+
if (Array.isArray(r) && r.length > 0) {
184+
return callback(undefined, r[0]);
185+
}
186+
187+
callback(undefined, host);
188+
});
189+
}

src/deps.ts

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ try {
2828
Kerberos = require('kerberos');
2929
} catch {} // eslint-disable-line
3030

31+
export interface KerberosClient {
32+
step: (challenge: string, callback?: Callback<string>) => Promise<string> | void;
33+
wrap: (
34+
challenge: string,
35+
options?: { user: string },
36+
callback?: Callback<string>
37+
) => Promise<string> | void;
38+
unwrap: (challenge: string, callback?: Callback<string>) => Promise<string> | void;
39+
}
40+
3141
export let Snappy: typeof import('snappy') = makeErrorModule(
3242
new MongoError(
3343
'Optional module `snappy` not found. Please install it to enable snappy compression'
@@ -124,21 +134,3 @@ export interface AutoEncrypter {
124134
encrypt(ns: string, cmd: Document, options: any, callback: Callback<Document>): void;
125135
decrypt(cmd: Document, options: any, callback: Callback<Document>): void;
126136
}
127-
128-
/** Declaration Merging block for MongoDB specific functionality in Kerberos */
129-
declare module 'kerberos' {
130-
export const processes: {
131-
MongoAuthProcess: {
132-
new (host: string, port: number, serviceName: string, options: unknown): {
133-
host: string;
134-
port: number;
135-
serviceName: string;
136-
canonicalizeHostName: boolean;
137-
retries: number;
138-
139-
init: (username: string, password: string, callback: Callback) => void;
140-
transition: (payload: unknown, callback: Callback) => void;
141-
};
142-
};
143-
};
144-
}

test/unit/optional_require.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe('optionalRequire', function () {
4141
return this.skip();
4242
}
4343
const gssapi = new GSSAPI();
44-
gssapi.auth(new AuthContext(null, true, { host: true, port: true }), error => {
44+
gssapi.prepare('', new AuthContext(null, true, { host: true, port: true }), error => {
4545
expect(error).to.exist;
4646
expect(error.message).includes('not found');
4747
});

0 commit comments

Comments
 (0)