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

Support MySQL 8 Authentication #2233

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ matrix:
env: "DOCKER_MYSQL_TYPE=mysql DOCKER_MYSQL_VERSION=5.5"
- node_js: *lts
env: "DOCKER_MYSQL_TYPE=mysql DOCKER_MYSQL_VERSION=5.6"
- node_js: *lts
env: "DOCKER_MYSQL_TYPE=mysql DOCKER_MYSQL_VERSION=8.0"
- node_js: *lts
env: "DOCKER_MYSQL_TYPE=mariadb DOCKER_MYSQL_VERSION=5.5"
- node_js: *lts
Expand Down
90 changes: 89 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- [Community](#community)
- [Establishing connections](#establishing-connections)
- [Connection options](#connection-options)
- [Authentication options](#authentication-options)
- [SSL options](#ssl-options)
- [Terminating connections](#terminating-connections)
- [Pooling connections](#pooling-connections)
Expand Down Expand Up @@ -235,6 +236,7 @@ issue [#501](https://github.com/mysqljs/mysql/issues/501). (Default: `false`)
also possible to blacklist default ones. For more information, check
[Connection Flags](#connection-flags).
* `ssl`: object with ssl parameters or a string containing name of ssl profile. See [SSL options](#ssl-options).
* `secureAuth`: required to support `caching_sha2_password` handshakes over insecure connections (default behavior on MySQL 8.0.4 or higher). See [Authentication options](#authentication-options).


In addition to passing these options as an object, you can also use a url
Expand All @@ -247,6 +249,82 @@ var connection = mysql.createConnection('mysql://user:pass@host/db?debug=true&ch
Note: The query values are first attempted to be parsed as JSON, and if that
fails assumed to be plaintext strings.

### Authentication options

MySQL 8.0 introduces a new default authentication plugin - [`caching_sha2_password`](https://dev.mysql.com/doc/refman/8.0/en/caching-sha2-pluggable-authentication.html).
This is a breaking change from MySQL 5.7 wherein [`mysql_native_password`](https://dev.mysql.com/doc/refman/8.0/en/native-pluggable-authentication.html) was used by default.

The initial handshake for this plugin will only work if the connection is secure or the server
uses a valid RSA public key for the given type of authentication (both default MySQL 8 settings).
By default, if the connection is not secure, the client will fetch the public key from the server
and use it (alongside a server-generated nonce) to encrypt the password.

After a successful initial handshake, any subsequent handshakes will always work, until the
server shuts down or the password is somehow removed from the server authentication cache.

The default connection options provide compatibility with both MySQL 5.7 and MySQL 8 servers.

```js
// default options
var connection = mysql.createConnection({
ssl : false,
secureAuth : true
});
```

If you are in control of the server public key, you can also provide it explicitly and avoid
the additional round-trip.

```js
var connection = mysql.createConnection({
ssl : false,
secureAuth : {
key: fs.readFileSync(__dirname + '/mysql-pub.key')
}
});
```

As an alternative to providing just the key, you can provide additional options, in the same
format as [crypto.publicEncrypt](https://nodejs.org/docs/latest-v4.x/api/crypto.html#crypto_crypto_publicencrypt_public_key_buffer),
which means you can also specify the key padding type.

**Caution** MySQL 8.0.4 specifically requires `RSA_PKCS1_PADDING` whereas MySQL 8.0.11 GA (and above) require `RSA_PKCS1_OAEP_PADDING` (which is the default value).

```js
var constants = require('constants');

var connection = mysql.createConnection({
ssl : false,
secureAuth : {
key: fs.readFileSync(__dirname + '/mysql-pub.key'),
padding: constants.RSA_PKCS1_PADDING
}
});
```

At least one of these options needs to be enabled for the initial handshake to work. So, the
following flavour will also work.

```js
var connection = mysql.createConnection({
ssl : true, // or a valid ssl configuration object
secureAuth : false
});
```

If both `secureAuth` and `ssl` options are disabled, the connection will fail.

```js
var connection = mysql.createConnection({
ssl : false,
secureAuth : false
});

connection.connect(function (err) {
console.log(err.message); // 'Authentication requires secure connection'
});
```

### SSL options

The `ssl` option in the connection options takes a string or an object. When given a string,
Expand Down Expand Up @@ -560,6 +638,7 @@ The available options for this feature are:
* `password`: The password of the new user (defaults to the previous one).
* `charset`: The new charset (defaults to the previous one).
* `database`: The new database (defaults to the previous one).
* `timeout`: An optional [timeout](#timeouts).

A sometimes useful side effect of this functionality is that this function also
resets any connection state (variables, transactions, etc.).
Expand Down Expand Up @@ -611,7 +690,7 @@ connection.query('SELECT * FROM `books` WHERE `author` = ?', ['David'], function
The third form `.query(options, callback)` comes when using various advanced
options on the query, like [escaping query values](#escaping-query-values),
[joins with overlapping column names](#joins-with-overlapping-column-names),
[timeouts](#timeout), and [type casting](#type-casting).
[timeouts](#timeouts), and [type casting](#type-casting).

```js
connection.query({
Expand Down Expand Up @@ -1393,12 +1472,21 @@ The following flags are sent by default on a new connection:
- `LONG_PASSWORD` - Use the improved version of Old Password Authentication.
- `MULTI_RESULTS` - Can handle multiple resultsets for COM_QUERY.
- `ODBC` Old; no effect.
- `PLUGIN_AUTH` - Support different authentication plugins.
- `PROTOCOL_41` - Uses the 4.1 protocol.
- `PS_MULTI_RESULTS` - Can handle multiple resultsets for COM_STMT_EXECUTE.
- `RESERVED` - Old flag for the 4.1 protocol.
- `SECURE_CONNECTION` - Support native 4.1 authentication.
- `TRANSACTIONS` - Asks for the transaction status flags.

The `local_infile` system variable is disabled by default since MySQL 8.0.2, which
means the `LOCAL_FILES` flag will only make sense if the feature is explicitely
enabled on the server.

```sql
SET GLOBAL local_infile = true;
```

In addition, the following flag will be sent if the option `multipleStatements`
is set to `true`:

Expand Down
4 changes: 3 additions & 1 deletion lib/ConnectionConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ function ConnectionConfig(options) {
// Set the client flags
var defaultFlags = ConnectionConfig.getDefaultFlags(options);
this.clientFlags = ConnectionConfig.mergeFlags(defaultFlags, options.flags);

this.secureAuth = options.secureAuth !== undefined ? options.secureAuth : true;
}

ConnectionConfig.mergeFlags = function mergeFlags(defaultFlags, userFlags) {
Expand Down Expand Up @@ -106,7 +108,7 @@ ConnectionConfig.getDefaultFlags = function getDefaultFlags(options) {
'+LONG_PASSWORD', // Use the improved version of Old Password Authentication
'+MULTI_RESULTS', // Can handle multiple resultsets for COM_QUERY
'+ODBC', // Special handling of ODBC behaviour
'-PLUGIN_AUTH', // Does *NOT* support auth plugins
'+PLUGIN_AUTH', // Supports auth plugins
'+PROTOCOL_41', // Uses the 4.1 protocol
'+PS_MULTI_RESULTS', // Can handle multiple resultsets for COM_STMT_EXECUTE
'+RESERVED', // Unused
Expand Down
42 changes: 40 additions & 2 deletions lib/protocol/Auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,35 @@ function auth(name, data, options) {
options = options || {};

switch (name) {
case 'caching_sha2_password':
return Auth.sha2Token(options.password, data.slice(0, 20));
case 'mysql_native_password':
return Auth.token(options.password, data.slice(0, 20));
case 'mysql_old_password':
return Auth.scramble323(data.slice(0, 20), options.password);
default:
return undefined;
}
}
Auth.auth = auth;

function sha1(msg) {
var hash = Crypto.createHash('sha1');
function createHash(msg, algorithm) {
algorithm = algorithm || 'sha1';
var hash = Crypto.createHash(algorithm);
hash.update(msg, 'binary');
return hash.digest('binary');
}

function sha1(msg) {
return createHash(msg, 'sha1');
}

function sha256(msg) {
return createHash(msg, 'sha256');
}

Auth.sha1 = sha1;
Auth.sha256 = sha256;

function xor(a, b) {
a = Buffer.from(a, 'binary');
Expand All @@ -44,6 +59,29 @@ Auth.token = function(password, scramble) {
return xor(stage3, stage1);
};

Auth.sha2Token = function(password, scramble) {
if (!password) {
return Buffer.alloc(0);
}

// password must be in binary format, not utf8
var stage1 = sha256((Buffer.from(password, 'utf8')).toString('binary'));
var stage2 = sha256(stage1);
var stage3 = sha256(stage2 + scramble.toString('binary'));
return xor(stage1, stage3);
};

Auth.encrypt = function(password, scramble, key) {
if (typeof Crypto.publicEncrypt !== 'function') {
var err = new Error('The Node.js version does not support public key encryption');
err.code = 'PUB_KEY_ENCRYPTION_NOT_AVAILABLE';
throw err;
}

var stage1 = xor((Buffer.from(password + '\0', 'utf8')).toString('binary'), scramble.toString('binary'));
Copy link
Member

@sidorares sidorares Oct 24, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs to be changed to rolling xor, otherwise it'll fail for password longer than 19 characters

See discussion at sidorares/node-mysql2#1044

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I've been working to fix this PR up over the past week and I did notice that long passwords didn't work on the new auth. There are a bunch of other little minor issues I've been finding as well. I didn't intend to list them out and was just intending to push up all the changes here then merge, but let me know if you think I should list them all out in addition to pushing up the fixes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that mysql2 is using slightly different api for this. No config required, and if you need to use non-defaults you pass custom configured plugin as authPlugins: { caching_sha2_password: XXXX }. Should not be a big problems for user migrating both ways mysql<->mysql2

return Crypto.publicEncrypt(key, stage1);
};

// This is a port of sql/password.c:hash_password which needs to be used for
// pre-4.1 passwords.
Auth.hashPassword = function(password) {
Expand Down
17 changes: 17 additions & 0 deletions lib/protocol/packets/AuthMoreDataPacket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module.exports = AuthMoreDataPacket;
function AuthMoreDataPacket(options) {
options = options || {};

this.status = 0x01;
this.data = options.data;
}

AuthMoreDataPacket.prototype.parse = function parse(parser) {
this.status = parser.parseUnsignedNumber(1);
this.data = parser.parsePacketTerminatedString();
};

AuthMoreDataPacket.prototype.write = function parse(writer) {
writer.writeUnsignedNumber(this.status);
writer.writeString(this.data);
};
8 changes: 8 additions & 0 deletions lib/protocol/packets/ClearTextPasswordPacket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = ClearTextPasswordPacket;
function ClearTextPasswordPacket(options) {
this.data = options.data;
}

ClearTextPasswordPacket.prototype.write = function write(writer) {
writer.writeNullTerminatedString(this.data);
};
3 changes: 3 additions & 0 deletions lib/protocol/packets/ComChangeUserPacket.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ function ComChangeUserPacket(options) {
this.scrambleBuff = options.scrambleBuff;
this.database = options.database;
this.charsetNumber = options.charsetNumber;
this.authPlugin = options.authPlugin;
}

ComChangeUserPacket.prototype.parse = function(parser) {
Expand All @@ -15,6 +16,7 @@ ComChangeUserPacket.prototype.parse = function(parser) {
this.scrambleBuff = parser.parseLengthCodedBuffer();
this.database = parser.parseNullTerminatedString();
this.charsetNumber = parser.parseUnsignedNumber(1);
this.authPlugin = parser.parseNullTerminatedString();
};

ComChangeUserPacket.prototype.write = function(writer) {
Expand All @@ -23,4 +25,5 @@ ComChangeUserPacket.prototype.write = function(writer) {
writer.writeLengthCodedBuffer(this.scrambleBuff);
writer.writeNullTerminatedString(this.database);
writer.writeUnsignedNumber(2, this.charsetNumber);
writer.writeNullTerminatedString(this.authPlugin);
};
15 changes: 15 additions & 0 deletions lib/protocol/packets/FastAuthSuccessPacket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = FastAuthSuccessPacket;
function FastAuthSuccessPacket() {
this.status = 0x01;
this.authMethodName = 0x03;
}

FastAuthSuccessPacket.prototype.parse = function parse(parser) {
this.status = parser.parseUnsignedNumber(1);
this.authMethodName = parser.parseUnsignedNumber(1);
};

FastAuthSuccessPacket.prototype.write = function write(writer) {
writer.writeUnsignedNumber(1, this.status);
writer.writeUnsignedNumber(1, this.authMethodName);
};
12 changes: 12 additions & 0 deletions lib/protocol/packets/HandshakeResponse41Packet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = HandshakeResponse41Packet;
function HandshakeResponse41Packet() {
this.status = 0x02;
}

HandshakeResponse41Packet.prototype.parse = function write(parser) {
this.status = parser.parseUnsignedNumber(1);
};

HandshakeResponse41Packet.prototype.write = function write(writer) {
writer.writeUnsignedNumber(1, this.status);
};
15 changes: 15 additions & 0 deletions lib/protocol/packets/PerformFullAuthenticationPacket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = PerformFullAuthenticationPacket;
function PerformFullAuthenticationPacket() {
this.status = 0x01;
this.authMethodName = 0x04;
}

PerformFullAuthenticationPacket.prototype.parse = function parse(parser) {
this.status = parser.parseUnsignedNumber(1);
this.authMethodName = parser.parseUnsignedNumber(1);
};

PerformFullAuthenticationPacket.prototype.write = function write(writer) {
writer.writeUnsignedNumber(1, this.status);
writer.writeUnsignedNumber(1, this.authMethodName);
};
5 changes: 5 additions & 0 deletions lib/protocol/packets/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
exports.AuthMoreDataPacket = require('./AuthMoreDataPacket');
exports.AuthSwitchRequestPacket = require('./AuthSwitchRequestPacket');
exports.AuthSwitchResponsePacket = require('./AuthSwitchResponsePacket');
exports.ClearTextPasswordPacket = require('./ClearTextPasswordPacket');
exports.ClientAuthenticationPacket = require('./ClientAuthenticationPacket');
exports.ComChangeUserPacket = require('./ComChangeUserPacket');
exports.ComPingPacket = require('./ComPingPacket');
Expand All @@ -9,12 +11,15 @@ exports.ComStatisticsPacket = require('./ComStatisticsPacket');
exports.EmptyPacket = require('./EmptyPacket');
exports.EofPacket = require('./EofPacket');
exports.ErrorPacket = require('./ErrorPacket');
exports.FastAuthSuccessPacket = require('./FastAuthSuccessPacket');
exports.Field = require('./Field');
exports.FieldPacket = require('./FieldPacket');
exports.HandshakeInitializationPacket = require('./HandshakeInitializationPacket');
exports.HandshakeResponse41Packet = require('./HandshakeResponse41Packet');
exports.LocalDataFilePacket = require('./LocalDataFilePacket');
exports.OkPacket = require('./OkPacket');
exports.OldPasswordPacket = require('./OldPasswordPacket');
exports.PerformFullAuthenticationPacket = require('./PerformFullAuthenticationPacket');
exports.ResultSetHeaderPacket = require('./ResultSetHeaderPacket');
exports.RowDataPacket = require('./RowDataPacket');
exports.SSLRequestPacket = require('./SSLRequestPacket');
Expand Down
Loading