Skip to content

Commit 7504e3d

Browse files
committed
Merge commit '1044a14745b97a43a5887d3c586b1d3a022d5154'
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
2 parents d6dd8e1 + 1044a14 commit 7504e3d

34 files changed

+1057
-27
lines changed

.travis.yml

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ matrix:
3131
env: "DOCKER_MYSQL_TYPE=mysql DOCKER_MYSQL_VERSION=5.5"
3232
- node_js: *lts
3333
env: "DOCKER_MYSQL_TYPE=mysql DOCKER_MYSQL_VERSION=5.6"
34+
- node_js: *lts
35+
env: "DOCKER_MYSQL_TYPE=mysql DOCKER_MYSQL_VERSION=8.0"
3436
- node_js: *lts
3537
env: "DOCKER_MYSQL_TYPE=mariadb DOCKER_MYSQL_VERSION=5.5"
3638
- node_js: *lts

Readme.md

+89-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- [Community](#community)
1717
- [Establishing connections](#establishing-connections)
1818
- [Connection options](#connection-options)
19+
- [Authentication options](#authentication-options)
1920
- [SSL options](#ssl-options)
2021
- [Terminating connections](#terminating-connections)
2122
- [Pooling connections](#pooling-connections)
@@ -235,6 +236,7 @@ issue [#501](https://github.com/mysqljs/mysql/issues/501). (Default: `false`)
235236
also possible to blacklist default ones. For more information, check
236237
[Connection Flags](#connection-flags).
237238
* `ssl`: object with ssl parameters or a string containing name of ssl profile. See [SSL options](#ssl-options).
239+
* `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).
238240

239241

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

252+
### Authentication options
253+
254+
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).
255+
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.
256+
257+
The initial handshake for this plugin will only work if the connection is secure or the server
258+
uses a valid RSA public key for the given type of authentication (both default MySQL 8 settings).
259+
By default, if the connection is not secure, the client will fetch the public key from the server
260+
and use it (alongside a server-generated nonce) to encrypt the password.
261+
262+
After a successful initial handshake, any subsequent handshakes will always work, until the
263+
server shuts down or the password is somehow removed from the server authentication cache.
264+
265+
The default connection options provide compatibility with both MySQL 5.7 and MySQL 8 servers.
266+
267+
```js
268+
// default options
269+
var connection = mysql.createConnection({
270+
ssl : false,
271+
secureAuth : true
272+
});
273+
```
274+
275+
If you are in control of the server public key, you can also provide it explicitly and avoid
276+
the additional round-trip.
277+
278+
```js
279+
var connection = mysql.createConnection({
280+
ssl : false,
281+
secureAuth : {
282+
key: fs.readFileSync(__dirname + '/mysql-pub.key')
283+
}
284+
});
285+
```
286+
287+
As an alternative to providing just the key, you can provide additional options, in the same
288+
format as [crypto.publicEncrypt](https://nodejs.org/docs/latest-v4.x/api/crypto.html#crypto_crypto_publicencrypt_public_key_buffer),
289+
which means you can also specify the key padding type.
290+
291+
**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).
292+
293+
```js
294+
var constants = require('constants');
295+
296+
var connection = mysql.createConnection({
297+
ssl : false,
298+
secureAuth : {
299+
key: fs.readFileSync(__dirname + '/mysql-pub.key'),
300+
padding: constants.RSA_PKCS1_PADDING
301+
}
302+
});
303+
```
304+
305+
At least one of these options needs to be enabled for the initial handshake to work. So, the
306+
following flavour will also work.
307+
308+
```js
309+
var connection = mysql.createConnection({
310+
ssl : true, // or a valid ssl configuration object
311+
secureAuth : false
312+
});
313+
```
314+
315+
If both `secureAuth` and `ssl` options are disabled, the connection will fail.
316+
317+
```js
318+
var connection = mysql.createConnection({
319+
ssl : false,
320+
secureAuth : false
321+
});
322+
323+
connection.connect(function (err) {
324+
console.log(err.message); // 'Authentication requires secure connection'
325+
});
326+
```
327+
250328
### SSL options
251329

252330
The `ssl` option in the connection options takes a string or an object. When given a string,
@@ -558,6 +636,7 @@ The available options for this feature are:
558636
* `password`: The password of the new user (defaults to the previous one).
559637
* `charset`: The new charset (defaults to the previous one).
560638
* `database`: The new database (defaults to the previous one).
639+
* `timeout`: An optional [timeout](#timeouts).
561640

562641
A sometimes useful side effect of this functionality is that this function also
563642
resets any connection state (variables, transactions, etc.).
@@ -1367,13 +1446,22 @@ The following flags are sent by default on a new connection:
13671446
- `LONG_PASSWORD` - Use the improved version of Old Password Authentication.
13681447
- `MULTI_RESULTS` - Can handle multiple resultsets for COM_QUERY.
13691448
- `ODBC` Old; no effect.
1449+
- `PLUGIN_AUTH` - Support different authentication plugins.
13701450
- `PROTOCOL_41` - Uses the 4.1 protocol.
13711451
- `PS_MULTI_RESULTS` - Can handle multiple resultsets for COM_STMT_EXECUTE.
13721452
- `RESERVED` - Old flag for the 4.1 protocol.
13731453
- `SECURE_CONNECTION` - Support native 4.1 authentication.
13741454
- `TRANSACTIONS` - Asks for the transaction status flags.
13751455

1376-
The following flag will be sent if the option `multipleStatements`
1456+
The `local_infile` system variable is disabled by default since MySQL 8.0.2, which
1457+
means the `LOCAL_FILES` flag will only make sense if the feature is explicitely
1458+
enabled on the server.
1459+
1460+
```sql
1461+
SET GLOBAL local_infile = true;
1462+
```
1463+
1464+
In addition, the following flag will be sent if the option `multipleStatements`
13771465
is set to `true`:
13781466

13791467
- `MULTI_STATEMENTS` - The client may send multiple statement per query or

lib/ConnectionConfig.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ function ConnectionConfig(options) {
6262
// Set the client flags
6363
var defaultFlags = ConnectionConfig.getDefaultFlags(options);
6464
this.clientFlags = ConnectionConfig.mergeFlags(defaultFlags, options.flags);
65+
66+
this.secureAuth = options.secureAuth !== undefined ? options.secureAuth : true;
6567
}
6668

6769
ConnectionConfig.mergeFlags = function mergeFlags(defaultFlags, userFlags) {
@@ -109,7 +111,7 @@ ConnectionConfig.getDefaultFlags = function getDefaultFlags(options) {
109111
'+LONG_PASSWORD', // Use the improved version of Old Password Authentication
110112
'+MULTI_RESULTS', // Can handle multiple resultsets for COM_QUERY
111113
'+ODBC', // Special handling of ODBC behaviour
112-
'-PLUGIN_AUTH', // Does *NOT* support auth plugins
114+
'+PLUGIN_AUTH', // Supports auth plugins
113115
'+PROTOCOL_41', // Uses the 4.1 protocol
114116
'+PS_MULTI_RESULTS', // Can handle multiple resultsets for COM_STMT_EXECUTE
115117
'+RESERVED', // Unused

lib/protocol/Auth.js

+40-2
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,35 @@ function auth(name, data, options) {
66
options = options || {};
77

88
switch (name) {
9+
case 'caching_sha2_password':
10+
return Auth.sha2Token(options.password, data.slice(0, 20));
911
case 'mysql_native_password':
1012
return Auth.token(options.password, data.slice(0, 20));
13+
case 'mysql_old_password':
14+
return Auth.scramble323(data.slice(0, 20), options.password);
1115
default:
1216
return undefined;
1317
}
1418
}
1519
Auth.auth = auth;
1620

17-
function sha1(msg) {
18-
var hash = Crypto.createHash('sha1');
21+
function createHash(msg, algorithm) {
22+
algorithm = algorithm || 'sha1';
23+
var hash = Crypto.createHash(algorithm);
1924
hash.update(msg, 'binary');
2025
return hash.digest('binary');
2126
}
27+
28+
function sha1(msg) {
29+
return createHash(msg, 'sha1');
30+
}
31+
32+
function sha256(msg) {
33+
return createHash(msg, 'sha256');
34+
}
35+
2236
Auth.sha1 = sha1;
37+
Auth.sha256 = sha256;
2338

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

62+
Auth.sha2Token = function(password, scramble) {
63+
if (!password) {
64+
return Buffer.alloc(0);
65+
}
66+
67+
// password must be in binary format, not utf8
68+
var stage1 = sha256((Buffer.from(password, 'utf8')).toString('binary'));
69+
var stage2 = sha256(stage1);
70+
var stage3 = sha256(stage2 + scramble.toString('binary'));
71+
return xor(stage1, stage3);
72+
};
73+
74+
Auth.encrypt = function(password, scramble, key) {
75+
if (typeof Crypto.publicEncrypt !== 'function') {
76+
var err = new Error('The Node.js version does not support public key encryption');
77+
err.code = 'PUB_KEY_ENCRYPTION_NOT_AVAILABLE';
78+
throw err;
79+
}
80+
81+
var stage1 = xor((Buffer.from(password + '\0', 'utf8')).toString('binary'), scramble.toString('binary'));
82+
return Crypto.publicEncrypt(key, stage1);
83+
};
84+
4785
// This is a port of sql/password.c:hash_password which needs to be used for
4886
// pre-4.1 passwords.
4987
Auth.hashPassword = function(password) {
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module.exports = AuthMoreDataPacket;
2+
function AuthMoreDataPacket(options) {
3+
options = options || {};
4+
5+
this.status = 0x01;
6+
this.data = options.data;
7+
}
8+
9+
AuthMoreDataPacket.prototype.parse = function parse(parser) {
10+
this.status = parser.parseUnsignedNumber(1);
11+
this.data = parser.parsePacketTerminatedString();
12+
};
13+
14+
AuthMoreDataPacket.prototype.write = function parse(writer) {
15+
writer.writeUnsignedNumber(this.status);
16+
writer.writeString(this.data);
17+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = ClearTextPasswordPacket;
2+
function ClearTextPasswordPacket(options) {
3+
this.data = options.data;
4+
}
5+
6+
ClearTextPasswordPacket.prototype.write = function write(writer) {
7+
writer.writeNullTerminatedString(this.data);
8+
};

lib/protocol/packets/ComChangeUserPacket.js

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ function ComChangeUserPacket(options) {
77
this.scrambleBuff = options.scrambleBuff;
88
this.database = options.database;
99
this.charsetNumber = options.charsetNumber;
10+
this.authPlugin = options.authPlugin;
1011
}
1112

1213
ComChangeUserPacket.prototype.parse = function(parser) {
@@ -15,6 +16,7 @@ ComChangeUserPacket.prototype.parse = function(parser) {
1516
this.scrambleBuff = parser.parseLengthCodedBuffer();
1617
this.database = parser.parseNullTerminatedString();
1718
this.charsetNumber = parser.parseUnsignedNumber(1);
19+
this.authPlugin = parser.parseNullTerminatedString();
1820
};
1921

2022
ComChangeUserPacket.prototype.write = function(writer) {
@@ -23,4 +25,5 @@ ComChangeUserPacket.prototype.write = function(writer) {
2325
writer.writeLengthCodedBuffer(this.scrambleBuff);
2426
writer.writeNullTerminatedString(this.database);
2527
writer.writeUnsignedNumber(2, this.charsetNumber);
28+
writer.writeNullTerminatedString(this.authPlugin);
2629
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module.exports = FastAuthSuccessPacket;
2+
function FastAuthSuccessPacket() {
3+
this.status = 0x01;
4+
this.authMethodName = 0x03;
5+
}
6+
7+
FastAuthSuccessPacket.prototype.parse = function parse(parser) {
8+
this.status = parser.parseUnsignedNumber(1);
9+
this.authMethodName = parser.parseUnsignedNumber(1);
10+
};
11+
12+
FastAuthSuccessPacket.prototype.write = function write(writer) {
13+
writer.writeUnsignedNumber(1, this.status);
14+
writer.writeUnsignedNumber(1, this.authMethodName);
15+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module.exports = HandshakeResponse41Packet;
2+
function HandshakeResponse41Packet() {
3+
this.status = 0x02;
4+
}
5+
6+
HandshakeResponse41Packet.prototype.parse = function write(parser) {
7+
this.status = parser.parseUnsignedNumber(1);
8+
};
9+
10+
HandshakeResponse41Packet.prototype.write = function write(writer) {
11+
writer.writeUnsignedNumber(1, this.status);
12+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module.exports = PerformFullAuthenticationPacket;
2+
function PerformFullAuthenticationPacket() {
3+
this.status = 0x01;
4+
this.authMethodName = 0x04;
5+
}
6+
7+
PerformFullAuthenticationPacket.prototype.parse = function parse(parser) {
8+
this.status = parser.parseUnsignedNumber(1);
9+
this.authMethodName = parser.parseUnsignedNumber(1);
10+
};
11+
12+
PerformFullAuthenticationPacket.prototype.write = function write(writer) {
13+
writer.writeUnsignedNumber(1, this.status);
14+
writer.writeUnsignedNumber(1, this.authMethodName);
15+
};

lib/protocol/packets/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
exports.AuthMoreDataPacket = require('./AuthMoreDataPacket');
12
exports.AuthSwitchRequestPacket = require('./AuthSwitchRequestPacket');
23
exports.AuthSwitchResponsePacket = require('./AuthSwitchResponsePacket');
4+
exports.ClearTextPasswordPacket = require('./ClearTextPasswordPacket');
35
exports.ClientAuthenticationPacket = require('./ClientAuthenticationPacket');
46
exports.ComChangeUserPacket = require('./ComChangeUserPacket');
57
exports.ComPingPacket = require('./ComPingPacket');
@@ -9,13 +11,16 @@ exports.ComStatisticsPacket = require('./ComStatisticsPacket');
911
exports.EmptyPacket = require('./EmptyPacket');
1012
exports.EofPacket = require('./EofPacket');
1113
exports.ErrorPacket = require('./ErrorPacket');
14+
exports.FastAuthSuccessPacket = require('./FastAuthSuccessPacket');
1215
exports.Field = require('./Field');
1316
exports.FieldPacket = require('./FieldPacket');
1417
exports.HandshakeInitializationPacket = require('./HandshakeInitializationPacket');
18+
exports.HandshakeResponse41Packet = require('./HandshakeResponse41Packet');
1519
exports.LocalDataFilePacket = require('./LocalDataFilePacket');
1620
exports.LocalInfileRequestPacket = require('./LocalInfileRequestPacket');
1721
exports.OkPacket = require('./OkPacket');
1822
exports.OldPasswordPacket = require('./OldPasswordPacket');
23+
exports.PerformFullAuthenticationPacket = require('./PerformFullAuthenticationPacket');
1924
exports.ResultSetHeaderPacket = require('./ResultSetHeaderPacket');
2025
exports.RowDataPacket = require('./RowDataPacket');
2126
exports.SSLRequestPacket = require('./SSLRequestPacket');

0 commit comments

Comments
 (0)