Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin based authentication support #331

Merged
merged 40 commits into from
Jun 29, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
42bfb5f
allow to pass connection handler as createServer parameter
sidorares Jun 20, 2016
0c4bad6
start using portfinder in tests
sidorares Jun 21, 2016
491e32d
connection.end() for server said of connections pair
sidorares Jun 21, 2016
1d4de1c
add connectAttributes config parameter
sidorares Jun 21, 2016
478de35
initial implementation of AuthSwitchRequest/Response/MoreData in hand…
sidorares Jun 21, 2016
06ceb9b
fix lint errors
sidorares Jun 21, 2016
039914f
typo in default auth plugin name
sidorares Jun 21, 2016
97f932f
typo in default auth plugin name
sidorares Jun 21, 2016
2e8aa56
use PLUGIN_AUTH flag by default only when authSwitchHandler connect p…
Jun 21, 2016
6afe56f
change_user command flow when plugin auth is enabled
Jun 21, 2016
63e1325
cleanup debug output
sidorares Jun 21, 2016
fa2661b
debug
sidorares Jun 21, 2016
086042d
christmas tree
sidorares Jun 22, 2016
cf9851f
fix typos
sidorares Jun 22, 2016
a2c8fd9
bisect
sidorares Jun 22, 2016
0248454
use portfinder to allocate test server ports
Jun 22, 2016
0e350ce
fix failing tests
Jun 22, 2016
8f2bb33
don't crash in debug log if there is unexpected packet
Jun 22, 2016
1029ce4
don't crash in debug log if there is unexpected packet
Jun 22, 2016
82c8109
don't crash in debug log if there is unexpected packet
Jun 22, 2016
f1dfd67
don't crash in debug log if there is unexpected packet
Jun 22, 2016
4b73868
remove debug
Jun 22, 2016
b164242
node 0.10: buffer.fill() does not return ref to buffer
Jun 22, 2016
4d30a57
node 0.10: buffer.fill() does not return ref to buffer
Jun 22, 2016
c1b2e88
debug default flags
sidorares Jun 22, 2016
275e26b
debugging change-user test
sidorares Jun 23, 2016
5b0cb52
debug change-user
sidorares Jun 23, 2016
432ea64
debug change-user
sidorares Jun 23, 2016
27e0f0c
debug failing test only
sidorares Jun 28, 2016
f74f7b7
update example
sidorares Jun 28, 2016
08b2742
update example
sidorares Jun 28, 2016
be52853
typo
sidorares Jun 28, 2016
95ff96e
check server version
sidorares Jun 28, 2016
c97eed5
handle end of handshake if there is no switch-auth request
sidorares Jun 28, 2016
3901293
re-enable matrix
sidorares Jun 28, 2016
9bba258
fix lint error
sidorares Jun 28, 2016
202a5c8
debug failing test
sidorares Jun 28, 2016
7390981
set packet length during real serializing
sidorares Jun 28, 2016
49fe402
set mysql server tz offset to 0 in time-related tests
sidorares Jun 29, 2016
cccff5e
add auth-switch api to readme
sidorares Jun 29, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ node_js:
- '0.10'
- '0.12'
- '4.4'
- '5.11'
- '5.12'
- '6.2'


Expand Down
4 changes: 4 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
1.0.0-rc-6 ( 29/06/2016 )
- AuthSwitch support and partial support for
plugin-based authentication #331

1.0.0-rc-5 ( 16/06/2016 )
- Fix incorrect releasing of dead pool connections #326, #325
- Allow pool options to be specified as URL params #327
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,40 @@ co(function * () {
```
see examples in [/examples/promise-co-await](/examples/promise-co-await)

### Authentication switch request

During connection phase the server may ask client to switch to a different auth method.
If `authSwitchHandler` connection config option is set it must be a function that receive
switch request data and respond via callback. Note that if `mysql_native_password` method is
requested it will be handled internally according to [Authentication::Native41]( https://dev.mysql.com/doc/internals/en/secure-password-authentication.html#packet-Authentication::Native41) and
`authSwitchHandler` won't be invoked. `authSwitchHandler` MAY be called multiple times if
plugin algorithm requires multiple roundtrips of data exchange between client and server.
First invocation always has `({pluginName, pluginData})` signature, following calls - `({pluginData})`.
The client respond with opaque blob matching requested plugin via `callback(null, data: Buffer)`.

Example: (imaginary `ssh-key-auth` plugin) pseudo code

```js
var conn = mysql.createConnection({
user: 'test_user',
password: 'test',
database: 'test_database',
authSwitchHandler: function(data, cb) {
if (data.pluginName === 'ssh-key-auth') {
getPrivateKey((key) => {
var response = encrypt(key, data.pluginData);
// continue handshake by sending response data
// respond with error to propagate error to connect/changeUser handlers
cb(null, response);
})
}
}
});
```

Initial handshake always performed using `mysql_native_password` plugin. This will be possible to override in
the future versions.

### Named placeholders

You can use named placeholders for parameters by setting `namedPlaceholders` config value or query/execute time option. Named placeholders are converted to unnamed `?` on the client (mysql protocol does not support named parameters). If you reference parameter multiple times under the same name it is sent to server multiple times.
Expand Down
4 changes: 2 additions & 2 deletions examples/promise-co-await/await.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
var mysql = require('../../promise.js');

async function test() {
const c = await mysql.createConnection({ port: 3306, user: 'mycause_dev', namedPlaceholders: true, password: 'mycause' });
const c = await mysql.createConnection({ port: 3306, user: 'testuser', namedPlaceholders: true, password: 'testpassword' });
console.log('connected!');
const [rows, fields] = await c.query('show databases');
console.log(rows);
Expand All @@ -24,7 +24,7 @@ async function test() {
console.log(end - start);
await c.end();

const p = mysql.createPool({ port: 3306, user: 'mycause_dev', namedPlaceholders: true, password: 'mycause' });
const p = mysql.createPool({ port: 3306, user: 'testuser', namedPlaceholders: true, password: 'testpassword' });
console.log( await p.execute('select sleep(0.5)') );
console.log('after first pool sleep');
var start = +new Date()
Expand Down
9 changes: 6 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ exports.createPoolCluster = function (config) {
return new PoolCluster(config);
};

module.exports.createServer = function () {
module.exports.createServer = function (handler) {
var Server = require('./lib/server.js');
return new Server();
var s = new Server();
if (handler) {
s.on('connection', handler);
}
return s;
};

exports.escape = SqlString.escape;
Expand All @@ -43,4 +47,3 @@ exports.__defineGetter__('createPoolClusterPromise', function () {
});

module.exports.Types = require('./lib/constants/types.js');

43 changes: 21 additions & 22 deletions lib/commands/change_user.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,43 @@ var util = require('util');
var Command = require('./command.js');
var Packets = require('../packets/index.js');
var ClientConstants = require('../constants/client.js');
var ClientHandshake = require('./client_handshake.js');

function ChangeUser (options, callback)
{
this.onResult = callback;
this._user = options.user;
this._password = options.password;
this._database = options.database;
this._passwordSha1 = options.passwordSha1;
this._charsetNumber = options.charsetNumber;
this._currentConfig = options.currentConfig;
this.user = options.user;
this.password = options.password;
this.database = options.database;
this.passwordSha1 = options.passwordSha1;
this.charsetNumber = options.charsetNumber;
this.currentConfig = options.currentConfig;
Command.call(this);
}
util.inherits(ChangeUser, Command);

ChangeUser.prototype.handshakeResult = ClientHandshake.prototype.handshakeResult;
ChangeUser.prototype.calculateNativePasswordAuthToken = ClientHandshake.prototype.calculateNativePasswordAuthToken;

ChangeUser.prototype.start = function (packet, connection) {
var packet = new Packets.ChangeUser({
user : this._user,
database : this._database,
charsetNumber : this._charsetNumber,
password : this._password,
passwordSha1 : this._passwordSha1,
flags : connection.config.clientFlags,
user : this.user,
database : this.database,
charsetNumber : this.charsetNumber,
password : this.password,
passwordSha1 : this.passwordSha1,
authPluginData1 : connection._handshakePacket.authPluginData1,
authPluginData2 : connection._handshakePacket.authPluginData2
});
this._currentConfig.user = this._user;
this._currentConfig.password = this._password;
this._currentConfig.database = this._database;
this._currentConfig.charsetNumber = this._charsetNumber;
this.currentConfig.user = this.user;
this.currentConfig.password = this.password;
this.currentConfig.database = this.database;
this.currentConfig.charsetNumber = this.charsetNumber;
// reset prepared statements cache as all statements become invalid after changeUser
connection._statements = {};
connection.writePacket(packet.toPacket());
return ChangeUser.prototype.changeOk;
return ChangeUser.prototype.handshakeResult;
};

ChangeUser.prototype.changeOk = function (okPacket, connection) {
if (this.onResult) {
this.onResult(null);
}
return null;
};
module.exports = ChangeUser;
77 changes: 70 additions & 7 deletions lib/commands/client_handshake.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,26 @@ ClientHandshake.prototype.sendCredentials = function (connection) {
charsetNumber : connection.config.charsetNumber,
authPluginData1: this.handshake.authPluginData1,
authPluginData2: this.handshake.authPluginData2,
compress: connection.config.compress
compress: connection.config.compress,
connectAttributes: connection.config.connectAttributes
});
connection.writePacket(handshakeResponse.toPacket());
};

var auth41 = require('../auth_41.js');
ClientHandshake.prototype.calculateNativePasswordAuthToken = function (authPluginData) {
// TODO: dont split into authPluginData1 and authPluginData2, instead join when 1 & 2 received
var authPluginData1 = authPluginData.slice(0, 8);
var authPluginData2 = authPluginData.slice(8, 20);
var authToken;
if (this.passwordSha1) {
authToken = auth41.calculateTokenFromPasswordSha(this.passwordSha1, authPluginData1, authPluginData2);
} else {
authToken = auth41.calculateToken(this.password, authPluginData1, authPluginData2);
}
return authToken;
};

ClientHandshake.prototype.handshakeInit = function (helloPacket, connection) {
var command = this;

Expand Down Expand Up @@ -100,13 +115,61 @@ ClientHandshake.prototype.handshakeInit = function (helloPacket, connection) {
return ClientHandshake.prototype.handshakeResult;
};

ClientHandshake.prototype.handshakeResult = function (okPacket, connection) {
// error is already checked in base class. Done auth.
connection.authorized = true;
if (connection.config.compress) {
var enableCompression = require('../compressed_protocol.js').enableCompression;
enableCompression(connection);
ClientHandshake.prototype.handshakeResult = function (packet, connection) {
var marker = packet.peekByte();
if (marker === 0xfe || marker === 1) {
var asr, asrmd;
var authSwitchHandlerParams = {};
if (marker === 1) {
asrmd = Packets.AuthSwitchRequestMoreData.fromPacket(packet);
authSwitchHandlerParams.pluginData = asrmd.data;
} else {
asr = Packets.AuthSwitchRequest.fromPacket(packet);
authSwitchHandlerParams.pluginName = asr.pluginName;
authSwitchHandlerParams.pluginData = asr.pluginData;
}
if (authSwitchHandlerParams.pluginName == 'mysql_native_password') {
var authToken = this.calculateNativePasswordAuthToken(authSwitchHandlerParams.pluginData);
connection.writePacket(new Packets.AuthSwitchResponse(authToken).toPacket());
} else if (connection.config.authSwitchHandler) {
connection.config.authSwitchHandler(authSwitchHandlerParams, function (err, data) {
if (err) {
connection.emit('error', err);
return;
}
connection.writePacket(new Packets.AuthSwitchResponse(data).toPacket());
});
} else {
connection.emit('error', new Error('Server requires auth switch, but no auth switch handler provided'));
return null;
}
return ClientHandshake.prototype.handshakeResult;
}

if (marker !== 0) {
var err = new Error('Unexpected packet during handshake phase');
if (this.onResult) {
this.onResult(err);
} else {
connection.emit('error', err);
}
return null;
}

// this should be called from ClientHandshake command only
// and skipped when called from ChangeUser command
if (!connection.authorized) {
connection.authorized = true;
if (connection.config.compress) {
var enableCompression = require('../compressed_protocol.js').enableCompression;
enableCompression(connection);
}
}

if (this.onResult) {
this.onResult(null);
}
return null;
};

module.exports = ClientHandshake;
17 changes: 15 additions & 2 deletions lib/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ function Connection (opts)
handshakeCommand.on('end', function () {
connection._handshakePacket = handshakeCommand.handshake;
connection.threadId = handshakeCommand.handshake.connectionId;
connection.emit('connect', handshakeCommand.handshake)
connection.emit('connect', handshakeCommand.handshake);
});
this.addCommand(handshakeCommand);
}
Expand Down Expand Up @@ -249,7 +249,9 @@ Connection.prototype.handlePacket = function (packet) {
if (packet) {
console.log(' raw: ' + packet.buffer.slice(packet.offset, packet.offset + packet.length()).toString('hex'));
console.trace();
console.log(this._internalId + ' ' + this.connectionId + ' ==> ' + this._command._commandName + '#' + this._command.stateName() + '(' + [packet.sequenceId, packet.type(), packet.length()].join(',') + ')');
var commandName = this._command ? this._command._commandName : '(no command)';
var stateName = this._command ? this._command.stateName() : '(no command)';
console.log(this._internalId + ' ' + this.connectionId + ' ==> ' + commandName + '#' + stateName + '(' + [packet.sequenceId, packet.type(), packet.length()].join(',') + ')');
}
}
if (!this._command) {
Expand Down Expand Up @@ -649,6 +651,17 @@ Connection.prototype.serverHandshake = function serverHandshake (args) {
// TODO: domainify
Connection.prototype.end = function (callback) {
var connection = this;

if (this.config.isServer) {
connection._closing = true;
var quitCmd = new EventEmitter();
setImmediate(function () {
connection.stream.end();
quitCmd.emit('end');
});
return quitCmd;
}

// trigger error if more commands enqueued after end command
var quitCmd = this.addCommand(new Commands.Quit(callback));
connection.addCommand = function () {
Expand Down
14 changes: 13 additions & 1 deletion lib/connection_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ function ConnectionConfig (options) {
this.trace = options.trace !== false;
this.stringifyObjects = options.stringifyObjects || false;
this.timezone = options.timezone || 'local';
this.flags = options.flags || '';
this.queryFormat = options.queryFormat;
this.pool = options.pool || undefined;
this.ssl = (typeof options.ssl === 'string')
Expand Down Expand Up @@ -64,8 +63,12 @@ function ConnectionConfig (options) {

this.compress = options.compress || false;

this.authSwitchHandler = options.authSwitchHandler;

this.clientFlags = ConnectionConfig.mergeFlags(ConnectionConfig.getDefaultFlags(options),
options.flags || '');

this.connectAttributes = options.connectAttributes;
}

ConnectionConfig.mergeFlags = function (default_flags, user_flags) {
Expand Down Expand Up @@ -107,6 +110,15 @@ ConnectionConfig.getDefaultFlags = function (options) {
defaultFlags.push('MULTI_STATEMENTS');
}

if (options && options.authSwitchHandler) {
defaultFlags.push('PLUGIN_AUTH');
defaultFlags.push('PLUGIN_AUTH_LENENC_CLIENT_DATA');
}

if (options && options.connectAttributes) {
defaultFlags.push('CONNECT_ATTRS');
}

return defaultFlags;
};

Expand Down
37 changes: 37 additions & 0 deletions lib/packets/auth_switch_request.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest

var Packet = require('../packets/packet');

function AuthSwitchRequest (opts)
{
this.pluginName = opts.pluginName;
this.pluginData = opts.pluginData;
}

AuthSwitchRequest.prototype.toPacket = function ()
{
var length = 6 + this.pluginName.length + this.pluginData.length;
var buffer = new Buffer(length);
var packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeInt8(0xfe);
packet.writeNullTerminatedString(this.pluginName);
packet.writeBuffer(this.pluginData);
return packet;
};

AuthSwitchRequest.fromPacket = function (packet)
{
var marker = packet.readInt8();
// assert marker == 0xfe?

var name = packet.readNullTerminatedString();
var data = packet.readBuffer();

return new AuthSwitchRequest({
pluginName: name,
pluginData: data
});
};

module.exports = AuthSwitchRequest;
Loading