-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support speculative authentication in scram-sha and x509
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
Showing
16 changed files
with
531 additions
and
952 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.