Skip to content

Commit

Permalink
Support mysql_native_password auth switch request for Azure
Browse files Browse the repository at this point in the history
fixes #1396
fixes #1729
closes #1730
  • Loading branch information
bgrainger authored and dougwilson committed Aug 10, 2017
1 parent 525960c commit e8fea70
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 7 deletions.
1 change: 1 addition & 0 deletions Changes.md
Expand Up @@ -7,6 +7,7 @@ you spot any mistakes.
## HEAD

* Fix typo in insecure auth error message
* Support `mysql_native_password` auth switch request for Azure #1396 #1729 #1730

## v2.14.1 (2017-08-01)

Expand Down
5 changes: 5 additions & 0 deletions lib/protocol/Parser.js
Expand Up @@ -309,6 +309,11 @@ Parser.prototype._nullByteOffset = function() {
return offset;
};

Parser.prototype.parsePacketTerminatedBuffer = function parsePacketTerminatedBuffer() {
var length = this._packetEnd - this._offset;
return this.parseBuffer(length);
};

Parser.prototype.parsePacketTerminatedString = function() {
var length = this._packetEnd - this._offset;
return this.parseString(length);
Expand Down
20 changes: 20 additions & 0 deletions lib/protocol/packets/AuthSwitchRequestPacket.js
@@ -0,0 +1,20 @@
module.exports = AuthSwitchRequestPacket;
function AuthSwitchRequestPacket(options) {
options = options || {};

this.status = 0xfe;
this.authMethodName = options.authMethodName;
this.authMethodData = options.authMethodData;
}

AuthSwitchRequestPacket.prototype.parse = function parse(parser) {
this.status = parser.parseUnsignedNumber(1);
this.authMethodName = parser.parseNullTerminatedString();
this.authMethodData = parser.parsePacketTerminatedBuffer();
};

AuthSwitchRequestPacket.prototype.write = function write(writer) {
writer.writeUnsignedNumber(1, this.status);
writer.writeNullTerminatedString(this.authMethodName);
writer.writeBuffer(this.authMethodData);
};
14 changes: 14 additions & 0 deletions lib/protocol/packets/AuthSwitchResponsePacket.js
@@ -0,0 +1,14 @@
module.exports = AuthSwitchResponsePacket;
function AuthSwitchResponsePacket(options) {
options = options || {};

this.data = options.data;
}

AuthSwitchResponsePacket.prototype.parse = function parse(parser) {
this.data = parser.parsePacketTerminatedBuffer();
};

AuthSwitchResponsePacket.prototype.write = function write(writer) {
writer.writeBuffer(this.data);
};
2 changes: 2 additions & 0 deletions lib/protocol/packets/index.js
@@ -1,3 +1,5 @@
exports.AuthSwitchRequestPacket = require('./AuthSwitchRequestPacket');
exports.AuthSwitchResponsePacket = require('./AuthSwitchResponsePacket');
exports.ClientAuthenticationPacket = require('./ClientAuthenticationPacket');
exports.ComChangeUserPacket = require('./ComChangeUserPacket');
exports.ComPingPacket = require('./ComPingPacket');
Expand Down
31 changes: 29 additions & 2 deletions lib/protocol/sequences/Handshake.js
Expand Up @@ -15,7 +15,7 @@ function Handshake(options, callback) {
this._handshakeInitializationPacket = null;
}

Handshake.prototype.determinePacket = function(firstByte) {
Handshake.prototype.determinePacket = function determinePacket(firstByte, parser) {
if (firstByte === 0xff) {
return Packets.ErrorPacket;
}
Expand All @@ -25,12 +25,39 @@ Handshake.prototype.determinePacket = function(firstByte) {
}

if (firstByte === 0xfe) {
return Packets.UseOldPasswordPacket;
return (parser.packetLength() === 1)
? Packets.UseOldPasswordPacket
: Packets.AuthSwitchRequestPacket;
}

return undefined;
};

Handshake.prototype['AuthSwitchRequestPacket'] = function (packet) {
switch (packet.authMethodName) {
case 'mysql_native_password':
case 'mysql_old_password':
default:
}

if (packet.authMethodName === 'mysql_native_password') {
var challenge = packet.authMethodData.slice(0, 20);

this.emit('packet', new Packets.AuthSwitchResponsePacket({
data: Auth.token(this._config.password, challenge)
}));
} else {
var err = new Error(
'MySQL is requesting the ' + packet.authMethodName + ' authentication method, which is not supported.'
);

err.code = 'UNSUPPORTED_AUTH_METHOD';
err.fatal = true;

this.end(err);
}
};

Handshake.prototype['HandshakeInitializationPacket'] = function(packet) {
this._handshakeInitializationPacket = packet;

Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -6,6 +6,7 @@
"author": "Felix Geisendörfer <felix@debuggable.com> (http://debuggable.com/)",
"contributors": [
"Andrey Sidorov <sidorares@yandex.ru>",
"Bradley Grainger <bgrainger@gmail.com>",
"Douglas Christopher Wilson <doug@somethingdoug.com>",
"Diogo Resende <dresende@thinkdigital.pt>",
"Nathan Woltman <nwoltman@outlook.com>"
Expand Down
22 changes: 17 additions & 5 deletions test/FakeServer.js
Expand Up @@ -64,6 +64,7 @@ function FakeConnection(socket) {
this._handshakeInitializationPacket = null;
this._clientAuthenticationPacket = null;
this._oldPasswordPacket = null;
this._authSwitchResponse = null;
this._handshakeOptions = {};

socket.on('data', this._handleData.bind(this));
Expand Down Expand Up @@ -91,9 +92,7 @@ FakeConnection.prototype.deny = function(message, errno) {
}));
};

FakeConnection.prototype._sendAuthResponse = function(packet, expected) {
var got = packet.scrambleBuff;

FakeConnection.prototype._sendAuthResponse = function _sendAuthResponse(got, expected) {
if (expected.toString('hex') === got.toString('hex')) {
this._sendPacket(new Packets.OkPacket());
} else {
Expand Down Expand Up @@ -269,9 +268,11 @@ FakeConnection.prototype._parsePacket = function(header) {
this._clientAuthenticationPacket = packet;
if (this._handshakeOptions.oldPassword) {
this._sendPacket(new Packets.UseOldPasswordPacket());
} else if (this._handshakeOptions.authMethodName) {
this._sendPacket(new Packets.AuthSwitchRequestPacket(this._handshakeOptions));
} else if (this._handshakeOptions.password === 'passwd') {
var expected = Buffer.from('3DA0ADA7C9E1BB3A110575DF53306F9D2DE7FD09', 'hex');
this._sendAuthResponse(packet, expected);
this._sendAuthResponse(packet.scrambleBuff, expected);
} else if (this._handshakeOptions.user || this._handshakeOptions.password) {
throw new Error('not implemented');
} else {
Expand All @@ -287,7 +288,14 @@ FakeConnection.prototype._parsePacket = function(header) {

var expected = Auth.scramble323(this._handshakeInitializationPacket.scrambleBuff(), this._handshakeOptions.password);

this._sendAuthResponse(packet, expected);
this._sendAuthResponse(packet.scrambleBuff, expected);
break;
case Packets.AuthSwitchResponsePacket:
this._authSwitchResponse = packet;

var expected = Auth.token(this._handshakeOptions.password, Buffer.from('00112233445566778899AABBCCDDEEFF01020304', 'hex'));

this._sendAuthResponse(packet.data, expected);
break;
case Packets.ComQueryPacket:
if (!this.emit('query', packet)) {
Expand Down Expand Up @@ -355,6 +363,10 @@ FakeConnection.prototype._determinePacket = function(header) {
return Packets.OldPasswordPacket;
}

if (this._handshakeOptions.authMethodName && !this._authSwitchResponse) {
return Packets.AuthSwitchResponsePacket;
}

var firstByte = this._parser.peak();
switch (firstByte) {
case 0x01: return Packets.ComQuitPacket;
Expand Down
36 changes: 36 additions & 0 deletions test/unit/connection/test-auth-switch-native.js
@@ -0,0 +1,36 @@
var assert = require('assert');
var Buffer = require('safe-buffer').Buffer;
var common = require('../../common');
var connection = common.createConnection({
port : common.fakeServerPort,
password : 'authswitch'
});

var server = common.createFakeServer();

var connected;
server.listen(common.fakeServerPort, function (err) {
assert.ifError(err);

connection.connect(function (err, result) {
assert.ifError(err);

connected = result;

connection.destroy();
server.destroy();
});
});

server.on('connection', function(incomingConnection) {
incomingConnection.handshake({
user : connection.config.user,
password : connection.config.password,
authMethodName : 'mysql_native_password',
authMethodData : Buffer.from('00112233445566778899AABBCCDDEEFF0102030400', 'hex')
});
});

process.on('exit', function() {
assert.equal(connected.fieldCount, 0);
});
32 changes: 32 additions & 0 deletions test/unit/connection/test-auth-switch-unknown.js
@@ -0,0 +1,32 @@
var assert = require('assert');
var Buffer = require('safe-buffer').Buffer;
var common = require('../../common');
var connection = common.createConnection({
port : common.fakeServerPort,
password : 'authswitch'
});

var server = common.createFakeServer();

server.listen(common.fakeServerPort, function (err) {
assert.ifError(err);

connection.connect(function (err) {
assert.ok(err);
assert.equal(err.code, 'UNSUPPORTED_AUTH_METHOD');
assert.equal(err.fatal, true);
assert.ok(/foo_plugin_password/.test(err.message));

connection.destroy();
server.destroy();
});
});

server.on('connection', function(incomingConnection) {
incomingConnection.handshake({
user : connection.config.user,
password : connection.config.password,
authMethodName : 'foo_plugin_password',
authMethodData : Buffer.alloc(0)
});
});

0 comments on commit e8fea70

Please sign in to comment.