diff --git a/.eslintrc.json b/.eslintrc.json index 8938b77..7dc4755 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -10,7 +10,7 @@ "es6": true }, "parserOptions": { - "ecmaVersion": 2019 + "ecmaVersion": 2023 }, "plugins": [ "prettier" diff --git a/README.md b/README.md index 9421d30..6dc0cd5 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ NOTE: The test suite requires an active kerberos deployment. ## Functions
-
checkPassword(username, password, service, [defaultRealm], [callback])Promise
+
checkPassword(username, password, service, [defaultRealm])Promise.<null>

This function provides a simple way to verify that a user name and password match those normally used for Kerberos authentication. It does this by checking that the supplied user name and password can be @@ -141,14 +141,14 @@ has the correct realms and KDCs listed.

only be used for testing. Do not use this in any production system - your security could be compromised if you do.

-
principalDetails(service, hostname, [callback])Promise
+
principalDetails(service, hostname)Promise

This function returns the service principal for the server given a service type and hostname.

Details are looked up via the /etc/keytab file.

-
initializeClient(service, [options], [callback])Promise
+
initializeClient(service, [options])Promise.<KerberosClient>

Initializes a context for client-side authentication with the given service principal.

-
initializeServer(service, [callback])Promise
+
initializeServer(service)Promise.<KerberosServer>

Initializes a context for server-side authentication with the given service principal.

@@ -168,52 +168,46 @@ security could be compromised if you do.

* [KerberosClient](#KerberosClient) - * [.step(challenge, [callback])](#KerberosClient+step) + * [.step(challenge)](#KerberosClient+step) - * [.wrap(challenge, [options], [callback])](#KerberosClient+wrap) + * [.wrap(challenge, [options])](#KerberosClient+wrap) - * [.unwrap(challenge, [callback])](#KerberosClient+unwrap) + * [.unwrap(challenge)](#KerberosClient+unwrap) -### *kerberosClient*.step(challenge, [callback]) +### *kerberosClient*.step(challenge) | Param | Type | Description | | --- | --- | --- | | challenge | string | A string containing the base64-encoded server data (which may be empty for the first step) | -| [callback] | function | | Processes a single kerberos client-side step using the supplied server challenge. -**Returns**: Promise - returns Promise if no callback passed -### *kerberosClient*.wrap(challenge, [options], [callback]) +### *kerberosClient*.wrap(challenge, [options]) | Param | Type | Description | | --- | --- | --- | | challenge | string | The response returned after calling `unwrap` | -| [options] | object | Optional settings | +| [options] | object | Options | | [options.user] | string | The user to authorize | | [options.protect] | boolean | Indicates if the wrap should request message confidentiality | -| [callback] | function | | Perform the client side kerberos wrap step. -**Returns**: Promise - returns Promise if no callback passed -### *kerberosClient*.unwrap(challenge, [callback]) +### *kerberosClient*.unwrap(challenge) | Param | Type | Description | | --- | --- | --- | | challenge | string | A string containing the base64-encoded server data | -| [callback] | function | | Perform the client side kerberos unwrap step -**Returns**: Promise - returns Promise if no callback passed ## KerberosServer @@ -228,19 +222,17 @@ Perform the client side kerberos unwrap step -### *kerberosServer*.step(challenge, [callback]) +### *kerberosServer*.step(challenge) | Param | Type | Description | | --- | --- | --- | | challenge | string | A string containing the base64-encoded client data | -| [callback] | function | | Processes a single kerberos server-side step using the supplied client data. -**Returns**: Promise - returns Promise if no callback passed -## checkPassword(username, password, service, [defaultRealm], [callback]) +## checkPassword(username, password, service, [defaultRealm]) | Param | Type | Description | | --- | --- | --- | @@ -248,7 +240,6 @@ Processes a single kerberos server-side step using the supplied client data. | password | string | The password for the user. | | service | string | The Kerberos service to check access for. | | [defaultRealm] | string | The default realm to use if one is not supplied in the user argument. | -| [callback] | function | | This function provides a simple way to verify that a user name and password match those normally used for Kerberos authentication. @@ -266,25 +257,24 @@ IMPORTANT: This method is vulnerable to KDC spoofing attacks and it should only be used for testing. Do not use this in any production system - your security could be compromised if you do. -**Returns**: Promise - returns Promise if no callback passed +**Returns**: Promise.<null> - returns Promise that rejects if the password is invalid -## principalDetails(service, hostname, [callback]) +## principalDetails(service, hostname) | Param | Type | Description | | --- | --- | --- | | service | string | The Kerberos service type for the server. | | hostname | string | The hostname of the server. | -| [callback] | function | | This function returns the service principal for the server given a service type and hostname. Details are looked up via the `/etc/keytab` file. -**Returns**: Promise - returns Promise if no callback passed +**Returns**: Promise - returns Promise -## initializeClient(service, [options], [callback]) +## initializeClient(service, [options]) | Param | Type | Description | | --- | --- | --- | @@ -293,20 +283,18 @@ Details are looked up via the `/etc/keytab` file. | [options.principal] | string | Optional string containing the client principal in the form 'user@realm' (e.g. 'jdoe@example.com'). | | [options.flags] | number | Optional integer used to set GSS flags. (e.g. `GSS_C_DELEG_FLAG\|GSS_C_MUTUAL_FLAG\|GSS_C_SEQUENCE_FLAG` will allow for forwarding credentials to the remote host) | | [options.mechOID] | number | Optional GSS mech OID. Defaults to None (GSS_C_NO_OID). Other possible values are `GSS_MECH_OID_KRB5`, `GSS_MECH_OID_SPNEGO`. | -| [callback] | function | | Initializes a context for client-side authentication with the given service principal. -**Returns**: Promise - returns Promise if no callback passed +**Returns**: [Promise.<KerberosClient>](#KerberosClient) - returns Promise -## initializeServer(service, [callback]) +## initializeServer(service) | Param | Type | Description | | --- | --- | --- | | service | string | A string containing the service principal in the form 'type@fqdn' (e.g. 'imap@mail.apple.com'). | -| [callback] | function | | Initializes a context for server-side authentication with the given service principal. -**Returns**: Promise - returns Promise if no callback passed +**Returns**: [Promise.<KerberosServer>](#KerberosServer) - returns Promise diff --git a/lib/auth_processes/mongodb.js b/lib/auth_processes/mongodb.js deleted file mode 100644 index 5682e11..0000000 --- a/lib/auth_processes/mongodb.js +++ /dev/null @@ -1,161 +0,0 @@ -'use strict'; -const dns = require('dns'); -const kerberos = require('../kerberos'); - -/** - * A class that was used for MongoDB kerberos authentication with legacy - * MongoDB Node drivers (`mongodb<4.0`). - * - * Not intended for direct use. - * - * @kind class - * - * @deprecated This class will be removed in an upcoming major release. - */ -class MongoAuthProcess { - constructor(host, port, serviceName, options) { - options = options || {}; - this.host = host; - this.port = port; - - // Set up service name - this.serviceName = serviceName || options.gssapiServiceName || 'mongodb'; - - // Options - this.canonicalizeHostName = - typeof options.gssapiCanonicalizeHostName === 'boolean' - ? options.gssapiCanonicalizeHostName - : false; - - // Set up first transition - this._transition = firstTransition(this); - - // Number of retries - this.retries = 10; - } - - init(username, password, callback) { - const self = this; - this.username = username; - this.password = password; - - // Canonicialize host name if needed - function performGssapiCanonicalizeHostName(canonicalizeHostName, host, callback) { - if (!canonicalizeHostName) return callback(); - - // 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) { - self.host = r[0]; - } - - callback(); - }); - } - - // Canonicialize host name if needed - performGssapiCanonicalizeHostName(this.canonicalizeHostName, this.host, err => { - if (err) return callback(err); - - const initOptions = {}; - if (password != null) { - Object.assign(initOptions, { user: username, password }); - } - - const service = - process.platform === 'win32' - ? `${this.serviceName}/${this.host}` - : `${this.serviceName}@${this.host}`; - - kerberos.initializeClient(service, initOptions, (err, client) => { - if (err) return callback(err, null); - - self.client = client; - callback(null, client); - }); - }); - } - - transition(payload, callback) { - if (this._transition == null) { - return callback(new Error('Transition finished')); - } - - this._transition(payload, callback); - } -} - -function firstTransition(auth) { - return (payload, callback) => { - auth.client.step('', (err, response) => { - if (err) return callback(err); - - // Set up the next step - auth._transition = secondTransition(auth); - - // Return the payload - callback(null, response); - }); - }; -} - -function secondTransition(auth) { - return (payload, callback) => { - auth.client.step(payload, (err, response) => { - if (err && auth.retries === 0) return callback(err); - - // Attempt to re-establish a context - if (err) { - // Adjust the number of retries - auth.retries = auth.retries - 1; - - // Call same step again - return auth.transition(payload, callback); - } - - // Set up the next step - auth._transition = thirdTransition(auth); - - // Return the payload - callback(null, response || ''); - }); - }; -} - -function thirdTransition(auth) { - return (payload, callback) => { - // GSS Client Unwrap - auth.client.unwrap(payload, (err, response) => { - if (err) return callback(err, false); - - // Wrap the response - auth.client.wrap(response, { user: auth.username }, (err, wrapped) => { - if (err) return callback(err, false); - - // Set up the next step - auth._transition = fourthTransition(auth); - - // Return the payload - callback(null, wrapped); - }); - }); - }; -} - -function fourthTransition(auth) { - return (payload, callback) => { - // Set the transition to null - auth._transition = null; - - // Callback with valid authentication - callback(null, true); - }; -} - -// Set the process -module.exports = { - MongoAuthProcess -}; diff --git a/lib/index.js b/lib/index.js index 2359362..d3b3fd2 100644 --- a/lib/index.js +++ b/lib/index.js @@ -9,8 +9,3 @@ module.exports = kerberos; module.exports.Kerberos = kerberos; module.exports.version = require('../package.json').version; - -// Set up the auth processes -module.exports.processes = { - MongoAuthProcess: require('./auth_processes/mongodb').MongoAuthProcess -}; diff --git a/lib/kerberos.js b/lib/kerberos.js index f768180..c69105a 100644 --- a/lib/kerberos.js +++ b/lib/kerberos.js @@ -1,6 +1,7 @@ 'use strict'; -const { loadBindings, defineOperation } = require('./util'); +const { promisify } = require('util'); +const { loadBindings } = require('./util'); const kerberos = loadBindings(); const KerberosClient = kerberos.KerberosClient; @@ -31,52 +32,59 @@ const GSS_MECH_OID_SPNEGO = 6; * @property {boolean} contextComplete Indicates that authentication has successfully completed or not */ +const promisifiedStep = promisify(KerberosClient.prototype.step); /** * Processes a single kerberos client-side step using the supplied server challenge. * * @kind function * @memberof KerberosClient * @param {string} challenge A string containing the base64-encoded server data (which may be empty for the first step) - * @param {function} [callback] - * @return {Promise} returns Promise if no callback passed + * @return {Promise} */ -KerberosClient.prototype.step = defineOperation(KerberosClient.prototype.step, [ - { name: 'challenge', type: 'string' }, - { name: 'callback', type: 'function', required: false } -]); +KerberosClient.prototype.step = async function step(challenge) { + if (typeof challenge !== 'string') { + throw new TypeError('parameter `challenge` must be a string.'); + } + return await promisifiedStep.call(this, challenge); +}; +const promsifiedWrap = promisify(KerberosClient.prototype.wrap); /** * Perform the client side kerberos wrap step. * * @kind function * @memberof KerberosClient * @param {string} challenge The response returned after calling `unwrap` - * @param {object} [options] Optional settings + * @param {object} [options] Options * @param {string} [options.user] The user to authorize * @param {boolean} [options.protect] Indicates if the wrap should request message confidentiality - * @param {function} [callback] - * @return {Promise} returns Promise if no callback passed + * @return {Promise} */ -KerberosClient.prototype.wrap = defineOperation(KerberosClient.prototype.wrap, [ - { name: 'challenge', type: 'string' }, - { name: 'options', type: 'object' }, - { name: 'callback', type: 'function', required: false } -]); +KerberosClient.prototype.wrap = async function wrap(challenge, options = {}) { + if (typeof challenge !== 'string') { + throw new TypeError('parameter `challenge` must be a string.'); + } + return await promsifiedWrap.call(this, challenge, options); +}; + +const promisifiedUnwrap = promisify(KerberosClient.prototype.unwrap); /** * Perform the client side kerberos unwrap step * * @kind function * @memberof KerberosClient * @param {string} challenge A string containing the base64-encoded server data - * @param {function} [callback] - * @return {Promise} returns Promise if no callback passed + * @return {Promise} */ -KerberosClient.prototype.unwrap = defineOperation(KerberosClient.prototype.unwrap, [ - { name: 'challenge', type: 'string' }, - { name: 'callback', type: 'function', required: false } -]); +KerberosClient.prototype.unwrap = async function unwrap(challenge) { + if (typeof challenge !== 'string') { + throw new TypeError('parameter `challenge` must be a string.'); + } + return await promisifiedUnwrap.call(this, challenge); +}; +const promisifiedServerStep = promisify(KerberosServer.prototype.step); /** * @class KerberosServer * @@ -92,13 +100,16 @@ KerberosClient.prototype.unwrap = defineOperation(KerberosClient.prototype.unwra * @kind function * @memberof KerberosServer * @param {string} challenge A string containing the base64-encoded client data - * @param {function} [callback] - * @return {Promise} returns Promise if no callback passed + * @return {Promise} */ -KerberosServer.prototype.step = defineOperation(KerberosServer.prototype.step, [ - { name: 'challenge', type: 'string' }, - { name: 'callback', type: 'function', required: false } -]); +KerberosServer.prototype.step = async function step(challenge) { + if (typeof challenge !== 'string') { + throw new TypeError('parameter `challenge` must be a string.'); + } + return await promisifiedServerStep.call(this, challenge); +}; + +const promisifiedCheckPassword = promisify(kerberos.checkPassword); /** * This function provides a simple way to verify that a user name and password @@ -122,17 +133,25 @@ KerberosServer.prototype.step = defineOperation(KerberosServer.prototype.step, [ * @param {string} password The password for the user. * @param {string} service The Kerberos service to check access for. * @param {string} [defaultRealm] The default realm to use if one is not supplied in the user argument. - * @param {function} [callback] - * @return {Promise} returns Promise if no callback passed + * @return {Promise} returns Promise that rejects if the password is invalid */ -const checkPassword = defineOperation(kerberos.checkPassword, [ - { name: 'username', type: 'string' }, - { name: 'password', type: 'string' }, - { name: 'service', type: 'string' }, - { name: 'defaultRealm', type: 'string', required: false }, - { name: 'callback', type: 'function', required: false } -]); - +async function checkPassword(username, password, service, defaultRealm) { + if (typeof username !== 'string') { + throw new TypeError('parameter `username` must be a string.'); + } + if (typeof password !== 'string') { + throw new TypeError('parameter `password` must be a string.'); + } + if (typeof service !== 'string') { + throw new TypeError('parameter `service` must be a string.'); + } + if (defaultRealm && typeof defaultRealm !== 'string') { + throw new TypeError('if specified, parameter `defaultRealm` must be a string.'); + } + return await promisifiedCheckPassword.call(this, username, password, service, defaultRealm); +} + +const promisifiedPrincipalDetails = promisify(kerberos.principalDetails); /** * This function returns the service principal for the server given a service type and hostname. * @@ -141,15 +160,19 @@ const checkPassword = defineOperation(kerberos.checkPassword, [ * @kind function * @param {string} service The Kerberos service type for the server. * @param {string} hostname The hostname of the server. - * @param {function} [callback] - * @return {Promise} returns Promise if no callback passed + * @return {Promise} returns Promise */ -const principalDetails = defineOperation(kerberos.principalDetails, [ - { name: 'service', type: 'string' }, - { name: 'hostname', type: 'string' }, - { name: 'callback', type: 'function', required: false } -]); - +async function principalDetails(service, hostname) { + if (typeof service !== 'string') { + throw new TypeError('parameter `service` must be a string.'); + } + if (typeof hostname !== 'string') { + throw new TypeError('parameter `hostname` must be a string.'); + } + return await promisifiedPrincipalDetails.call(this, service, hostname); +} + +const promisifiedInitializeClient = promisify(kerberos.initializeClient); /** * Initializes a context for client-side authentication with the given service principal. * @@ -159,27 +182,30 @@ const principalDetails = defineOperation(kerberos.principalDetails, [ * @param {string} [options.principal] Optional string containing the client principal in the form 'user@realm' (e.g. 'jdoe@example.com'). * @param {number} [options.flags] Optional integer used to set GSS flags. (e.g. `GSS_C_DELEG_FLAG\|GSS_C_MUTUAL_FLAG\|GSS_C_SEQUENCE_FLAG` will allow for forwarding credentials to the remote host) * @param {number} [options.mechOID] Optional GSS mech OID. Defaults to None (GSS_C_NO_OID). Other possible values are `GSS_MECH_OID_KRB5`, `GSS_MECH_OID_SPNEGO`. - * @param {function} [callback] - * @return {Promise} returns Promise if no callback passed + * @return {Promise} returns Promise */ -const initializeClient = defineOperation(kerberos.initializeClient, [ - { name: 'service', type: 'string' }, - { name: 'options', type: 'object', default: { mechOID: GSS_C_NO_OID } }, - { name: 'callback', type: 'function', required: false } -]); +async function initializeClient(service, options = { mechOID: GSS_C_NO_OID }) { + if (typeof service !== 'string') { + throw new TypeError('parameter `service` must be a string.'); + } + return await promisifiedInitializeClient.call(this, service, options); +} + +const promisifiedInitializeServer = promisify(kerberos.initializeServer); /** * Initializes a context for server-side authentication with the given service principal. * * @kind function * @param {string} service A string containing the service principal in the form 'type@fqdn' (e.g. 'imap@mail.apple.com'). - * @param {function} [callback] - * @return {Promise} returns Promise if no callback passed + * @return {Promise} returns Promise */ -const initializeServer = defineOperation(kerberos.initializeServer, [ - { name: 'service', type: 'string' }, - { name: 'callback', type: 'function', required: false } -]); +async function initializeServer(service) { + if (typeof service !== 'string') { + throw new TypeError('parameter `service` must be a string.'); + } + return await promisifiedInitializeServer.call(this, service); +} module.exports = { initializeClient, diff --git a/lib/util.js b/lib/util.js index 7b34acd..83ff146 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,86 +1,5 @@ 'use strict'; -function validateParameter(parameter, specs, specIndex) { - const spec = specs[specIndex]; - if (parameter == null && spec.required === false) { - return; - } - - if (parameter == null) { - throw new TypeError(`Required parameter \`${spec.name}\` missing`); - } - - const paramType = typeof parameter; - if (spec.type && paramType !== spec.type) { - if (spec.required === false) { - if (specs.slice(specIndex).some(def => def.type === paramType)) { - return false; - } - } - - throw new TypeError( - `Invalid type for parameter \`${spec.name}\`, expected \`${ - spec.type - }\` but found \`${typeof parameter}\`` - ); - } - - return true; -} - -function hasOwnProperty(object, property) { - return Object.prototype.hasOwnProperty.call(object, property); -} - -/** - * Monkey-patches an existing method to support parameter validation, as well - * as adding support for returning Promises if callbacks are not provided. - * - * @private - * @param {function} fn the function to override - * @param {Array} paramDefs the definitions of each parameter to the function - */ -function defineOperation(fn, paramDefs) { - return function () { - const args = Array.prototype.slice.call(arguments); - const params = []; - for (let i = 0, argIdx = 0; i < paramDefs.length; ++i, ++argIdx) { - const def = paramDefs[i]; - let arg = args[argIdx]; - - if (hasOwnProperty(def, 'default') && arg == null) arg = def.default; - if (def.type === 'object' && def.default != null) { - arg = Object.assign({}, def.default, arg); - } - - // special case to allow `options` to be optional - if (def.name === 'options' && (typeof arg === 'function' || arg == null)) { - arg = {}; - } - - if (validateParameter(arg, paramDefs, i)) { - params.push(arg); - } else { - argIdx--; - } - } - - const callback = arguments[arguments.length - 1]; - if (typeof callback !== 'function') { - return new Promise((resolve, reject) => { - params.push((err, response) => { - if (err) return reject(err); - resolve(response); - }); - - fn.apply(this, params); - }); - } - - fn.apply(this, params); - }; -} - function loadBindings() { try { return require('../build/Release/kerberos.node'); @@ -96,4 +15,4 @@ function loadBindings() { } } -module.exports = { defineOperation, validateParameter, loadBindings }; +module.exports = { loadBindings }; diff --git a/package-lock.json b/package-lock.json index d66007e..69337e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,14 +25,12 @@ "eslint-plugin-prettier": "^5.5.4", "jsdoc-to-markdown": "^9.1.2", "mocha": "^11.7.1", - "mongodb": "^6.18.0", "node-gyp": "^10.1.0", "prebuild": "^13.0.1", - "prettier": "^3.6.2", - "request": "^2.88.2" + "prettier": "^3.6.2" }, "engines": { - "node": ">=12.9.0" + "node": ">=20.19.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -375,16 +373,6 @@ "node": ">=v12.0.0" } }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", - "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", - "dev": true, - "license": "MIT", - "dependencies": { - "sparse-bitfield": "^3.0.3" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -543,21 +531,6 @@ "undici-types": "~7.10.0" } }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", - "dev": true - }, - "node_modules/@types/whatwg-url": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.3.tgz", - "integrity": "sha512-z1ELvMijRL1QmU7QuzDkeYXSF2+dXI0ITKoQsIoVKcNBOiK5RMmWy+pYYxJTHFt8vkpZe7UsvRErQwcxZkjoUw==", - "dev": true, - "dependencies": { - "@types/webidl-conversions": "*" - } - }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -935,16 +908,6 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, - "node_modules/bson": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", - "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.20.1" - } - }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -3459,13 +3422,6 @@ "dev": true, "license": "MIT" }, - "node_modules/memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "dev": true, - "license": "MIT" - }, "node_modules/memory-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/memory-stream/-/memory-stream-1.0.0.tgz", @@ -3849,63 +3805,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/mongodb": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz", - "integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.0" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, - "node_modules/mongodb-connection-string-url": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.0.tgz", - "integrity": "sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==", - "dev": true, - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^13.0.0" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5428,16 +5327,6 @@ "node": ">=0.10.0" } }, - "node_modules/sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "memory-pager": "^1.0.2" - } - }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -5792,18 +5681,6 @@ "node": ">=0.8" } }, - "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", - "dev": true, - "dependencies": { - "punycode": "^2.3.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -5990,28 +5867,6 @@ "node": ">=12.17" } }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", - "dev": true, - "dependencies": { - "tr46": "^4.1.1", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 9e2085d..6ab5950 100644 --- a/package.json +++ b/package.json @@ -42,11 +42,9 @@ "eslint-plugin-prettier": "^5.5.4", "jsdoc-to-markdown": "^9.1.2", "mocha": "^11.7.1", - "mongodb": "^6.18.0", "node-gyp": "^10.1.0", "prebuild": "^13.0.1", - "prettier": "^3.6.2", - "request": "^2.88.2" + "prettier": "^3.6.2" }, "overrides": { "prebuild": { diff --git a/test/defineOperation_tests.js b/test/defineOperation_tests.js deleted file mode 100644 index 3db5ebb..0000000 --- a/test/defineOperation_tests.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -const { loadBindings } = require('../lib/util'); - -const kerberos = loadBindings(); -const defineOperation = require('../lib/util').defineOperation; -const expect = require('chai').expect; - -const testMethod = defineOperation(kerberos._testMethod, [ - { name: 'string', type: 'string' }, - { name: 'shouldError', type: 'boolean', default: false }, - { name: 'optionalString', type: 'string', required: false }, - { name: 'callback', type: 'function', required: false } -]); - -describe('defineOperation', () => { - it('should validate parameters', function () { - expect(() => testMethod(42)).to.throw(/Invalid type for parameter/); - }); - - it('should validate optional parameters, with valid parameters after', function () { - expect(() => testMethod('llamas', false, true, () => {})).to.throw( - /Invalid type for parameter `optionalString`/ - ); - }); - - it('should support defaults', function (done) { - expect(() => testMethod('testing')).to.not.throw(); - testMethod('testing', true, err => { - expect(err).to.exist; - done(); - }); - }); - - it('should return a promise if no callback is provided', function () { - const promise = testMethod('llamas', false); - expect(promise).to.be.instanceOf(Promise); - }); - - it('should use a callback if provided', function (done) { - testMethod('testing', false, 'optional', (err, result) => { - expect(err).to.not.exist; - expect(result).to.equal('optional'); - - testMethod('testing', true, 'optional', (err, result) => { - expect(err).to.exist; - expect(result).to.not.exist; - - done(); - }); - }); - }); -}); diff --git a/test/kerberos_tests.js b/test/kerberos_tests.js index 4be9de6..bdbe20d 100644 --- a/test/kerberos_tests.js +++ b/test/kerberos_tests.js @@ -1,6 +1,5 @@ 'use strict'; const kerberos = require('../lib/index'); -const request = require('request'); const chai = require('chai'); const expect = chai.expect; const os = require('os'); @@ -19,109 +18,82 @@ describe('Kerberos', function () { if (os.type() === 'Windows_NT') this.skip(); }); - it('should lookup principal details on a server', function (done) { + it('should lookup principal details on a server', async function () { const expected = `HTTP/${hostname}@${realm.toUpperCase()}`; - kerberos.principalDetails('HTTP', hostname, (err, details) => { - expect(err).to.not.exist; - expect(details).to.equal(expected); - done(); - }); + const details = await kerberos.principalDetails('HTTP', hostname); + expect(details).to.equal(expected); }); - it('should check a given password against a kerberos server', function (done) { + it('should check a given password against a kerberos server', async function () { const service = `HTTP/${hostname}`; - kerberos.checkPassword(username, password, service, realm.toUpperCase(), err => { - expect(err).to.not.exist; - kerberos.checkPassword(username, 'incorrect-password', service, realm.toUpperCase(), err => { - expect(err).to.exist; - done(); - }); - }); + await kerberos.checkPassword(username, password, service, realm.toUpperCase()); + + const error = await kerberos + .checkPassword(username, 'incorrect-password', service, realm.toUpperCase()) + .catch(e => e); + expect(error).to.be.instanceOf(Error); }); - it('should authenticate against a kerberos server using GSSAPI', function (done) { + it('should authenticate against a kerberos server using GSSAPI', async function () { const service = `HTTP@${hostname}`; - kerberos.initializeClient(service, {}, (err, client) => { - expect(err).to.not.exist; - - kerberos.initializeServer(service, (err, server) => { - expect(err).to.not.exist; - expect(client.contextComplete).to.be.false; - expect(server.contextComplete).to.be.false; - - client.step('', (err, clientResponse) => { - expect(err).to.not.exist; - expect(client.contextComplete).to.be.false; - - server.step(clientResponse, (err, serverResponse) => { - expect(err).to.not.exist; - expect(client.contextComplete).to.be.false; - - client.step(serverResponse, err => { - expect(err).to.not.exist; - expect(client.contextComplete).to.be.true; - - const expectedUsername = `${username}@${realm.toUpperCase()}`; - expect(server.username).to.equal(expectedUsername); - expect(client.username).to.equal(expectedUsername); - expect(server.targetName).to.not.exist; - done(); - }); - }); - }); - }); - }); + const client = await kerberos.initializeClient(service, {}); + const server = await kerberos.initializeServer(service); + + expect(client.contextComplete).to.be.false; + expect(server.contextComplete).to.be.false; + + const clientResponse = await client.step(''); + expect(client.contextComplete).to.be.false; + + const serverResponse = await server.step(clientResponse); + + expect(client.contextComplete).to.be.false; + + await client.step(serverResponse); + expect(client.contextComplete).to.be.true; + + const expectedUsername = `${username}@${realm.toUpperCase()}`; + expect(server.username).to.equal(expectedUsername); + expect(client.username).to.equal(expectedUsername); + expect(server.targetName).to.not.exist; }); - it('should authenticate against a kerberos HTTP endpoint', function (done) { + it('should authenticate against a kerberos HTTP endpoint', async function () { const service = `HTTP@${hostname}`; const url = `http://${hostname}:${port}/`; // send the initial request un-authenticated - request.get(url, (err, response) => { - expect(err).to.not.exist; - expect(response).to.have.property('statusCode', 401); - - // validate the response supports the Negotiate protocol - const authenticateHeader = response.headers['www-authenticate']; - expect(authenticateHeader).to.exist; - expect(authenticateHeader).to.equal('Negotiate'); - - // generate the first Kerberos token - const mechOID = kerberos.GSS_MECH_OID_KRB5; - kerberos.initializeClient(service, { mechOID }, (err, client) => { - expect(err).to.not.exist; - - client.step('', (err, kerberosToken) => { - expect(err).to.not.exist; - - // attach the Kerberos token and resend back to the host - request.get( - { url, headers: { Authorization: `Negotiate ${kerberosToken}` } }, - (err, response) => { - expect(err).to.not.exist; - expect(response.statusCode).to.equal(200); - - // validate the headers exist and contain a www-authenticate message - const authenticateHeader = response.headers['www-authenticate']; - expect(authenticateHeader).to.exist; - expect(authenticateHeader).to.startWith('Negotiate'); - - // verify the return Kerberos token - const tokenParts = authenticateHeader.split(' '); - const serverKerberosToken = tokenParts[tokenParts.length - 1]; - client.step(serverKerberosToken, err => { - expect(err).to.not.exist; - expect(client.contextComplete).to.be.true; - done(); - }); - } - ); - }); - }); + const initialResponse = await fetch(url); + expect(initialResponse.status).to.equal(401); + + // validate the response supports the Negotiate protocol + const authenticateHeader = initialResponse.headers.get('www-authenticate'); + expect(authenticateHeader).to.exist; + expect(authenticateHeader).to.equal('Negotiate'); + + // generate the first Kerberos token + const mechOID = kerberos.GSS_MECH_OID_KRB5; + const client = await kerberos.initializeClient(service, { mechOID }); + const kerberosToken = await client.step(''); + + // attach the Kerberos token and resend back to the host + const authenticatedResponse = await fetch(url, { + headers: { Authorization: `Negotiate ${kerberosToken}` } }); + expect(authenticatedResponse.status).to.equal(200); + + // validate the headers exist and contain a www-authenticate message + const responseAuthHeader = authenticatedResponse.headers.get('www-authenticate'); + expect(responseAuthHeader).to.exist; + expect(responseAuthHeader).to.startWith('Negotiate'); + + // verify the return Kerberos token + const tokenParts = responseAuthHeader.split(' '); + const serverKerberosToken = tokenParts[tokenParts.length - 1]; + await client.step(serverKerberosToken); + expect(client.contextComplete).to.be.true; }); describe('Client.wrap()', function () { @@ -169,4 +141,84 @@ describe('Kerberos', function () { }); }); }); + + describe('parameter validation', function () { + test('initializeClient() throws if service is not a string', async function () { + expect(await kerberos.initializeClient().catch(e => e)) + .to.be.instanceOf(TypeError) + .to.match(/`service` must be a string/); + }); + + test('initializeServer() throws if service is not a string', async function () { + expect(await kerberos.initializeServer().catch(e => e)) + .to.be.instanceOf(TypeError) + .to.match(/`service` must be a string/); + }); + + test('principalDetails() throws if service is not a string', async function () { + expect(await kerberos.principalDetails(3, 'foo').catch(e => e)) + .to.be.instanceOf(TypeError) + .to.match(/`service` must be a string/); + }); + + test('principalDetails() throws if hostname is not a string', async function () { + expect(await kerberos.principalDetails('foo', 3).catch(e => e)) + .to.be.instanceOf(TypeError) + .to.match(/`hostname` must be a string/); + }); + + test('checkPassword() throws if username is not a string', async function () { + expect(await kerberos.checkPassword(3, 'password', 'service').catch(e => e)) + .to.be.instanceOf(TypeError) + .to.match(/`username` must be a string/); + }); + + test('checkPassword() throws if password is not a string', async function () { + expect(await kerberos.checkPassword('username', 3, 'service').catch(e => e)) + .to.be.instanceOf(TypeError) + .to.match(/`password` must be a string/); + }); + + test('checkPassword() throws if service is not a string', async function () { + expect(await kerberos.checkPassword('username', 'password', 3).catch(e => e)) + .to.be.instanceOf(TypeError) + .to.match(/`service` must be a string/); + }); + + test('KerberosServer.step() throws if challenge is not a string', async function () { + const service = `HTTP@${hostname}`; + + const server = await kerberos.initializeServer(service); + expect(await server.step(3).catch(e => e)) + .to.be.instanceOf(TypeError) + .to.match(/`challenge` must be a string/); + }); + + describe('KerberosClient', function () { + let client; + beforeEach(async function () { + const service = `HTTP@${hostname}`; + + client = await kerberos.initializeClient(service); + }); + + test('KerberosClient.unwrap() throws if challenge is not a string', async function () { + expect(await client.unwrap(3).catch(e => e)) + .to.be.instanceOf(TypeError) + .to.match(/`challenge` must be a string/); + }); + + test('KerberosClient.wrap() throws if challenge is not a string', async function () { + expect(await client.wrap(3).catch(e => e)) + .to.be.instanceOf(TypeError) + .to.match(/`challenge` must be a string/); + }); + + test('KerberosClient.step() throws if challenge is not a string', async function () { + expect(await client.step(3).catch(e => e)) + .to.be.instanceOf(TypeError) + .to.match(/`challenge` must be a string/); + }); + }); + }); }); diff --git a/test/kerberos_win32_tests.js b/test/kerberos_win32_tests.js deleted file mode 100644 index 550461d..0000000 --- a/test/kerberos_win32_tests.js +++ /dev/null @@ -1,191 +0,0 @@ -'use strict'; -const kerberos = require('../lib/index'); -const MongoClient = require('mongodb').MongoClient; -const expect = require('chai').expect; - -const password = process.env.KERBEROS_PASSWORD; -const realm = process.env.KERBEROS_REALM; -const hostname = process.env.KERBEROS_HOSTNAME; -const username = `${process.env.KERBEROS_USERNAME}@${realm}`; -const service = `mongodb/${hostname}`; -const port = process.env.KERBEROS_PORT || '27017'; -const upn = username; - -function authenticate(options, callback) { - const db = options.db; - const krbClient = options.krbClient; - const challenge = options.challenge || ''; - const start = options.start || false; - const conversationId = options.conversationId; - - let promise; - if (callback == null || typeof callback !== 'function') { - promise = new Promise((resolve, reject) => { - callback = function (err, res) { - if (err) return reject(err); - resolve(res); - }; - }); - } - - if (start) { - krbClient.step('', (err, payload) => { - expect(err).to.not.exist; - - db.command({ saslStart: 1, mechanism: 'GSSAPI', payload }, (err, dbResponse) => { - expect(err).to.not.exist; - - authenticate( - { - db, - krbClient, - challenge: dbResponse.payload, - conversationId: dbResponse.conversationId - }, - callback - ); - }); - }); - - return promise; - } - - krbClient.step(challenge, (err, payload) => { - payload = payload || ''; - - db.command({ saslContinue: 1, conversationId, payload }, (err, dbResponse) => { - if (krbClient.contextComplete) { - callback(null, { challenge: dbResponse.payload, conversationId }); - return; - } - - if (err) return callback(err, null); - authenticate({ db, krbClient, conversationId, challenge: payload }, callback); - }); - }); - - return promise; -} - -const test = {}; -describe('Kerberos (win32)', function () { - this.timeout(60000); - - beforeEach(function () { - if (process.platform !== 'win32') { - this.currentTest.skipReason = 'TODO(NODE-4021): Kerberos testing on windows'; - this.skip(); - } - }); - - beforeEach(function () { - test.client = new MongoClient(`mongodb://${hostname}:${port}/`); - }); - - afterEach(function () { - if (test.client) test.client.close().then(() => delete test.client); - }); - - it('should create a kerberos client', function () { - // this is a very basic test used to pass appveyor and provide prebuild binaries - return kerberos.initializeClient(service, { user: username, password }).then(krbClient => { - expect(krbClient).to.exist; - }); - }); - - it('should work from windows', function (done) { - test.client.connect((err, client) => { - expect(err).to.not.exist; - - const db = client.db('$external'); - - kerberos.initializeClient(service, { user: username, password }, (err, krbClient) => { - expect(err).to.not.exist; - - authenticate({ db, krbClient, start: true }, (err, authResponse) => { - expect(err).to.not.exist; - - krbClient.unwrap(authResponse.challenge, (err, unwrapped) => { - expect(err).to.not.exist; - - // RFC-4752 - const challengeBytes = Buffer.from(unwrapped, 'base64'); - expect(challengeBytes).to.have.length(4); - - // Manually create an authorization message and encrypt it. This - // is the "no security layer" message as detailed in RFC-4752, - // section 3.1, final paragraph. This is also the message created - // by calling authGSSClientWrap with the "user" option. - // const UPN = Buffer.from(upn, 'utf8').toString('utf8'); - const msg = Buffer.from(`\x01\x00\x00\x00${upn}`).toString('base64'); - krbClient.wrap(msg, (err, custom) => { - expect(err).to.not.exist; - expect(custom).to.exist; - - // Wrap using unwrapped and user principal - krbClient.wrap(unwrapped, { user: upn }, (err, wrapped) => { - expect(err).to.not.exist; - expect(wrapped).to.exist; - - db.command( - { - saslContinue: 1, - conversationId: authResponse.conversationId, - payload: wrapped - }, - err => { - expect(err).to.not.exist; - expect(krbClient.username).to.exist; - done(); - } - ); - }); - }); - }); - }); - }); - }); - }); - - it('should work from windows using promises', function () { - return test.client.connect().then(client => { - const db = client.db('$external'); - - return kerberos.initializeClient(service, { user: username, password }).then(krbClient => { - return authenticate({ db, krbClient, start: true }).then(authResponse => { - return krbClient.unwrap(authResponse.challenge).then(unwrapped => { - // RFC-4752 - const challengeBytes = Buffer.from(unwrapped, 'base64'); - expect(challengeBytes).to.have.length(4); - - // Manually create an authorization message and encrypt it. This - // is the "no security layer" message as detailed in RFC-4752, - // section 3.1, final paragraph. This is also the message created - // by calling authGSSClientWrap with the "user" option. - // const UPN = Buffer.from(upn, 'utf8').toString('utf8'); - const msg = Buffer.from(`\x01\x00\x00\x00${upn}`).toString('base64'); - return krbClient - .wrap(msg) - .then(custom => { - expect(custom).to.exist; - - // Wrap using unwrapped and user principal - return krbClient.wrap(unwrapped, { user: upn }); - }) - .then(wrapped => { - expect(wrapped).to.exist; - return db.command({ - saslContinue: 1, - conversationId: authResponse.conversationId, - payload: wrapped - }); - }) - .then(() => { - expect(krbClient.username).to.exist; - }); - }); - }); - }); - }); - }); -});