Skip to content

Commit

Permalink
feat: support speculative authentication in scram-sha and x509
Browse files Browse the repository at this point in the history
This introduces a new auth provider stage `prepare` which allows
a provider to alter the handshake document before its used for the
initial handshake. The SCRAM-SHA and X509 providers have been
improved to use the prepare stage in order to speculatively
authenticate, potentially reducing the roundtrips of connection
authentication.

NODE-2487
  • Loading branch information
mbroadst committed May 7, 2020
1 parent 17346cc commit f71f09b
Show file tree
Hide file tree
Showing 16 changed files with 531 additions and 952 deletions.
162 changes: 25 additions & 137 deletions lib/cmap/auth/auth_provider.js
Original file line number Diff line number Diff line change
@@ -1,163 +1,51 @@
'use strict';

const { MongoError } = require('../../error');

/**
* Creates a new AuthProvider, which dictates how to authenticate for a given
* mechanism.
* Context used during authentication
*
* @class
* @property {Connection} connection The connection to authenticate
* @property {MongoCredentials} credentials The credentials to use for authentication
* @property {object} options The options passed to the `connect` method
* @property {object?} response The response of the initial handshake
* @property {Buffer?} nonce A random nonce generated for use in an authentication conversation
*/
class AuthProvider {
constructor() {
this.authStore = [];
}

/**
* Authenticate
*
* @function
* @param {SendAuthCommand} sendAuthCommand Writes an auth command directly to a specific connection
* @param {Connection[]} connections Connections to authenticate using this authenticator
* @param {MongoCredentials} credentials Authentication credentials
* @param {authResultCallback} callback The callback to return the result from the authentication
*/
auth(sendAuthCommand, connections, credentials, callback) {
// Total connections
let count = connections.length;

if (count === 0) {
callback(null, null);
return;
}

// Valid connections
let numberOfValidConnections = 0;
let errorObject = null;

const execute = connection => {
this._authenticateSingleConnection(sendAuthCommand, connection, credentials, (err, r) => {
// Adjust count
count = count - 1;

// If we have an error
if (err) {
errorObject = new MongoError(err);
} else if (r && (r.$err || r.errmsg)) {
errorObject = new MongoError(r);
} else {
numberOfValidConnections = numberOfValidConnections + 1;
}

// Still authenticating against other connections.
if (count !== 0) {
return;
}

// We have authenticated all connections
if (numberOfValidConnections > 0) {
// Store the auth details
this.addCredentials(credentials);
// Return correct authentication
callback(null, true);
} else {
if (errorObject == null) {
errorObject = new MongoError(`failed to authenticate using ${credentials.mechanism}`);
}
callback(errorObject, false);
}
});
};

const executeInNextTick = _connection => process.nextTick(() => execute(_connection));

// For each connection we need to authenticate
while (connections.length > 0) {
executeInNextTick(connections.shift());
}
}

/**
* Implementation of a single connection authenticating. Is meant to be overridden.
* Will error if called directly
*/
_authenticateSingleConnection(/*sendAuthCommand, connection, credentials, callback*/) {
throw new Error('_authenticateSingleConnection must be overridden');
class AuthContext {
constructor(connection, credentials, options) {
this.connection = connection;
this.credentials = credentials;
this.options = options;
}
}

class AuthProvider {
/**
* Adds credentials to store only if it does not exist
* Prepare the handshake document before the initial handshake.
*
* @param {MongoCredentials} credentials credentials to add to store
* @param {object} handshakeDoc The document used for the initial handshake on a connection
* @param {AuthContext} authContext Context for authentication flow
* @param {Function} callback
*/
addCredentials(credentials) {
const found = this.authStore.some(cred => cred.equals(credentials));

if (!found) {
this.authStore.push(credentials);
}
prepare(handshakeDoc, authContext, callback) {
callback(undefined, handshakeDoc);
}

/**
* Re authenticate pool
* Authenticate
*
* @function
* @param {SendAuthCommand} sendAuthCommand Writes an auth command directly to a specific connection
* @param {Connection[]} connections Connections to authenticate using this authenticator
* @param {AuthContext} context A shared context for authentication flow
* @param {authResultCallback} callback The callback to return the result from the authentication
*/
reauthenticate(sendAuthCommand, connections, callback) {
const authStore = this.authStore.slice(0);
let count = authStore.length;
if (count === 0) {
return callback(null, null);
}

for (let i = 0; i < authStore.length; i++) {
this.auth(sendAuthCommand, connections, authStore[i], function(err) {
count = count - 1;
if (count === 0) {
callback(err, null);
}
});
}
}

/**
* Remove credentials that have been previously stored in the auth provider
*
* @function
* @param {string} source Name of database we are removing authStore details about
* @returns {void}
*/
logout(source) {
this.authStore = this.authStore.filter(credentials => credentials.source !== source);
auth(context, callback) {
callback(new TypeError('`auth` method must be overridden by subclass'));
}
}

/**
* A function that writes authentication commands to a specific connection
*
* @callback SendAuthCommand
* @param {Connection} connection The connection to write to
* @param {Command} command A command with a toBin method that can be written to a connection
* @param {AuthWriteCallback} callback Callback called when command response is received
*/

/**
* A callback for a specific auth command
*
* @callback AuthWriteCallback
* @param {Error} err If command failed, an error from the server
* @param {object} r The response from the server
*/

/**
* This is a result from an authentication strategy
* This is a result from an authentication provider
*
* @callback authResultCallback
* @param {error} error An error object. Set to null if no error present
* @param {boolean} result The result of the authentication process
*/

module.exports = { AuthProvider };
module.exports = { AuthContext, AuthProvider };
2 changes: 0 additions & 2 deletions lib/cmap/auth/defaultAuthProviders.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const MongoCR = require('./mongocr');
const X509 = require('./x509');
const Plain = require('./plain');
const GSSAPI = require('./gssapi');
const SSPI = require('./sspi');
const ScramSHA1 = require('./scram').ScramSHA1;
const ScramSHA256 = require('./scram').ScramSHA256;
const MongoDBAWS = require('./mongodb_aws');
Expand All @@ -21,7 +20,6 @@ function defaultAuthProviders() {
x509: new X509(),
plain: new Plain(),
gssapi: new GSSAPI(),
sspi: new SSPI(),
'scram-sha-1': new ScramSHA1(),
'scram-sha-256': new ScramSHA256()
};
Expand Down
Loading

0 comments on commit f71f09b

Please sign in to comment.