Skip to content

Commit

Permalink
Merge commit '1044a14745b97a43a5887d3c586b1d3a022d5154'
Browse files Browse the repository at this point in the history
Incorporated nwoltman's PR#2233 that adds support for authentication using the caching_sha2_password plugin which is the default in MySQL 8

mysqljs#2233
  • Loading branch information
vlasky committed Apr 16, 2021
2 parents d6dd8e1 + 1044a14 commit 7504e3d
Show file tree
Hide file tree
Showing 34 changed files with 1,057 additions and 27 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
Expand Up @@ -31,6 +31,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
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 @@ -558,6 +636,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 @@ -1367,13 +1446,22 @@ 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 following flag will be sent if the option `multipleStatements`
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`:

- `MULTI_STATEMENTS` - The client may send multiple statement per query or
Expand Down
4 changes: 3 additions & 1 deletion lib/ConnectionConfig.js
Expand Up @@ -62,6 +62,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 @@ -109,7 +111,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
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'));
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
@@ -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
@@ -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
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
@@ -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
@@ -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
@@ -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
@@ -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,13 +11,16 @@ 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.LocalInfileRequestPacket = require('./LocalInfileRequestPacket');
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

0 comments on commit 7504e3d

Please sign in to comment.