Skip to content

Commit

Permalink
feat(promises): allow to access all API by promise or callback
Browse files Browse the repository at this point in the history
  • Loading branch information
mbroadst committed Jul 31, 2018
1 parent 08857de commit 3b77430
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 69 deletions.
173 changes: 106 additions & 67 deletions lib/kerberos.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use strict';

const kerberos = require('bindings')('kerberos');
const KerberosClient = kerberos.KerberosClient;
const KerberosServer = kerberos.KerberosServer;

// Result Codes
const AUTH_GSS_CONTINUE = 0;
Expand Down Expand Up @@ -36,6 +38,93 @@ function validateParameter(parameter, spec) {
}
}

/// allows api methods to be called with a callback or a promise
function promisify(fn, paramDefs) {
return function() {
const args = Array.prototype.slice.call(arguments);
const params = [];
for (let i = 0; i < paramDefs.length; ++i) {
const def = paramDefs[i];
let arg = args[i];

if (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 = {};
}

validateParameter(arg, paramDefs[i]);
params.push(arg);
}

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);
});
}

params.push(callback);
fn.apply(this, params);
};
}

/**
* Processes a single GSSAPI client-side step using the supplied server data.
*
* @param {string} challenge A string containing the base64-encoded server data (which may be empty for the first step)
* @param {function} callback Returns a result code, or an error if one was
* @returns {string} response
*/
KerberosClient.prototype.step = promisify(KerberosClient.prototype.step, [
{ name: 'challenge', type: 'string' }
]);

/**
* Perform the client side GSSAPI wrap step.
*
* @memberof KerberosClient
* @param {string} challenge The result of the `authGSSClientResponse` after the `authGSSClientUnwrap`
* @param {object} [options] Optional settings
* @param {string} [options.user] The user to authorize
* @param {function} callback
*/
KerberosClient.prototype.wrap = promisify(KerberosClient.prototype.wrap, [
{ name: 'challenge', type: 'string' },
{ name: 'options', type: 'object' }
]);

/**
* Perform the client side GSSAPI unwrap step
*
* @memberof KerberosClient
* @param {string} challenge A string containing the base64-encoded server data
* @param {function} [callback]
*/
KerberosClient.prototype.unwrap = promisify(KerberosClient.prototype.unwrap, [
{ name: 'challenge', type: 'string' }
]);

/**
* Processes a single GSSAPI server-side step using the supplied client data.
*
* @memberof KerberosServer
* @param {string} challenge A string containing the base64-encoded client data
* @param {function} callback
*/
KerberosServer.prototype.step = promisify(KerberosServer.prototype.step, [
{ name: 'challenge', type: 'string' }
]);

/**
* This function provides a simple way to verify that a user name and password
* match those normally used for Kerberos authentication.
Expand All @@ -59,16 +148,12 @@ function validateParameter(parameter, spec) {
* @param {string} [defaultRealm] The default realm to use if one is not supplied in the user argument.
* @param {function} callback
*/
function checkPassword(username, password, service, defaultRealm, callback) {
if (typeof defaultRealm === 'function') (callback = defaultRealm), (defaultRealm = null);
validateParameter(username, { name: 'username', type: 'string' });
validateParameter(password, { name: 'password', type: 'string' });
validateParameter(service, { name: 'service', type: 'string' });
validateParameter(defaultRealm, { name: 'defaultRealm', type: 'string' });
validateParameter(callback, { name: 'callback', type: 'function' });

kerberos.checkPassword(username, password, service, defaultRealm, callback);
}
const checkPassword = promisify(kerberos.checkPassword, [
{ name: 'username', type: 'string' },
{ name: 'password', type: 'string' },
{ name: 'service', type: 'string' },
{ name: 'defaultRealm', type: 'string' }
]);

/**
* This function returns the service principal for the server given a service
Expand All @@ -80,13 +165,10 @@ function checkPassword(username, password, service, defaultRealm, callback) {
* @param {string} hostname The hostname of the server.
* @param {function} callback
*/
function principalDetails(service, hostname, callback) {
validateParameter(service, { name: 'service', type: 'string' });
validateParameter(hostname, { name: 'hostname', type: 'string' });
validateParameter(callback, { name: 'callback', type: 'function' });

kerberos.principalDetails(service, hostname, callback);
}
const principalDetails = promisify(kerberos.principalDetails, [
{ name: 'service', type: 'string' },
{ name: 'hostname', type: 'string' }
]);

/**
* The callback format for inserts
Expand All @@ -108,16 +190,10 @@ function principalDetails(service, hostname, callback) {
* @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 {initializeClientCallback} callback The operation callback
*/
function initializeClient(service, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
options = Object.assign({}, { mechOID: GSS_C_NO_OID }, options);

validateParameter(service, { name: 'service', type: 'string' });
validateParameter(options, { name: 'options', type: 'object' });
validateParameter(callback, { name: 'callback', type: 'function' });

kerberos.initializeClient(service, options, callback);
}
const initializeClient = promisify(kerberos.initializeClient, [
{ name: 'service', type: 'string' },
{ name: 'options', type: 'object', default: { mechOID: GSS_C_NO_OID } }
]);

/**
* Initializes a context for GSSAPI server-side authentication with the given
Expand All @@ -128,46 +204,9 @@ function initializeClient(service, options, callback) {
* @param {string} service A string containing the service principal in the form 'type@fqdn' (e.g. 'imap@mail.apple.com').
* @param {initializeServerCallback} callback
*/
function initializeServer(service, callback) {
validateParameter(service, { name: 'service', type: 'string' });
validateParameter(callback, { name: 'callback', type: 'function' });

kerberos.initializeServer(service, callback);
}

/**
* Processes a single GSSAPI client-side step using the supplied server data.
*
* @memberof KerberosClient
* @param {string} challenge A string containing the base64-encoded server data (which may be empty for the first step)
* @param {function} callback Returns a result code, or an error if one was encountered
*/

/**
* Perform the client side GSSAPI unwrap step
*
* @memberof KerberosClient
* @param {string} challenge A string containing the base64-encoded server data
* @param {function} callback
*/

/**
* Perform the client side GSSAPI wrap step.
*
* @memberof KerberosClient
* @param {string} challenge The result of the `authGSSClientResponse` after the `authGSSClientUnwrap`
* @param {object} [options] Optional settings
* @param {string} [options.user] The user to authorize
* @param {function} callback
*/

/**
* Processes a single GSSAPI server-side step using the supplied client data.
*
* @memberof KerberosServer
* @param {string} challenge A string containing the base64-encoded client data
* @param {function} callback
*/
const initializeServer = promisify(kerberos.initializeServer, [
{ name: 'service', type: 'string' }
]);

module.exports = {
initializeClient,
Expand Down
60 changes: 58 additions & 2 deletions test/kerberos_win32_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ function authenticate(options, callback) {
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) => {
db.command({ saslStart: 1, mechanism: 'GSSAPI', payload }, (err, dbResponse) => {
Expand All @@ -36,7 +46,7 @@ function authenticate(options, callback) {
});
});

return;
return promise;
}

krbClient.step(challenge, (err, payload) => {
Expand All @@ -52,6 +62,8 @@ function authenticate(options, callback) {
authenticate({ db, krbClient, conversationId, challenge: payload }, callback);
});
});

return promise;
}

const test = {};
Expand Down Expand Up @@ -97,7 +109,7 @@ describe('Kerberos (win32)', function() {
// 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) => {
krbClient.wrap(msg, (err, custom) => {
expect(err).to.not.exist;
expect(custom).to.exist;

Expand Down Expand Up @@ -126,4 +138,48 @@ describe('Kerberos (win32)', function() {
);
});
});

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, domain: realm, 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;
});
});
});
});
});
});
});

0 comments on commit 3b77430

Please sign in to comment.