Skip to content

Commit dadef38

Browse files
authored
Merge pull request #1436 from ruiquelhas/master
Add support for multi-factor authentication
2 parents 131c6b8 + f011524 commit dadef38

File tree

11 files changed

+507
-12
lines changed

11 files changed

+507
-12
lines changed

documentation/Authentication-Switch.md

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,46 @@
11
# Authentication switch request
22

3-
During connection phase the server may ask client to switch to a different auth method.
4-
If `authSwitchHandler` connection config option is set it must be a function that receive
5-
switch request data and respond via callback. Note that if `mysql_native_password` method is
6-
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
7-
`authSwitchHandler` won't be invoked. `authSwitchHandler` MAY be called multiple times if
8-
plugin algorithm requires multiple roundtrips of data exchange between client and server.
9-
First invocation always has `({pluginName, pluginData})` signature, following calls - `({pluginData})`.
10-
The client respond with opaque blob matching requested plugin via `callback(null, data: Buffer)`.
3+
During the connection phase the server may ask the client to switch to a different auth method.
4+
If the `authPlugins` connection config option is set, it must be an object where each key
5+
is the name of a potential authentication plugin requested by the server, and the corresponding
6+
value must be a function that optionally receives the connection config options and returns
7+
another function, which in turn, optionally receives the switch request data.
8+
9+
The plugin is loaded with a `({user,password,...})` signature, and each call has a `(pluginData)`
10+
signature. Each call should make the plugin return any additional authentication data (`Buffer`)
11+
that should be sent back to the server, either synchronously or asynchronously using a `Promise`,
12+
or should yield an error accordingly.
1113

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

16+
```js
17+
const conn = mysql.createConnection({
18+
user: 'test_user',
19+
password: 'test',
20+
database: 'test_database',
21+
authPlugins: {
22+
'ssh-key-auth': function ({password}) {
23+
return function (pluginData) {
24+
return getPrivate(key)
25+
.then(key => {
26+
const response = encrypt(key, password, pluginData);
27+
// continue handshake by sending response data
28+
return response;
29+
})
30+
.catch(err => {
31+
// throw error to propagate error to connect/changeUser handlers
32+
});
33+
};
34+
}
35+
}
36+
});
37+
```
38+
39+
There is also a deprecated API where if a `authSwitchHandler` connection config option is set
40+
it must be a function that receives switch request data and responds via a callback. In this case,
41+
the first invocation always has a `({pluginName, pluginData})` signature, following calls - `({pluginData})`.
42+
The client replies with an opaque blob matching the requested plugin via `callback(null, data: Buffer)`.
43+
1444
```js
1545
const conn = mysql.createConnection({
1646
user: 'test_user',
@@ -33,4 +63,47 @@ const conn = mysql.createConnection({
3363
});
3464
```
3565

36-
Initial handshake always performed using `mysql_native_password` plugin. This will be possible to override in the future versions.
66+
The initial handshake is always performed using `mysql_native_password` plugin. This will be possible to override in future versions.
67+
68+
Note that if the `mysql_native_password` method is requested it will be handled internally according
69+
to [Authentication::Native41]( https://dev.mysql.com/doc/internals/en/secure-password-authentication.html#packet-Authentication::Native41)
70+
and no `authPlugins` function or the `authSwitchHandler` will be invoked.
71+
72+
These MAY be called multiple times if the plugin algorithm requires multiple roundtrips of data
73+
exchange between client and server.
74+
75+
## Multi-factor authentication
76+
77+
If the user requires multi-factor authentication in the server, the client will receive a `AuthNextFactor`
78+
request, which is similar in structure to the regular authentication switch request and contains the name
79+
and possible initial data for the additional authentication factor plugin (up to 3). Additional passwords
80+
can be provided using the connection config options - `password2` and `password3`. Again, for each
81+
authentication factor, multiple roundtrips of data exchange can be required by the plugin algoritm.
82+
83+
```js
84+
const conn = mysql.createConnection({
85+
user: 'test_user',
86+
password: 'secret1',
87+
password2: 'secret2',
88+
password3: 'secret3',
89+
database: 'test_database',
90+
authPlugins: {
91+
// password1 === password
92+
'auth-plugin1': function ({password1}) {
93+
return function (serverPluginData) {
94+
return clientPluginData(password1, serverPluginData);
95+
};
96+
},
97+
'auth-plugin2': function ({password2}) {
98+
return function (serverPluginData) {
99+
return clientPluginData(password2, serverPluginData);
100+
};
101+
},
102+
'auth-plugin3': function ({password3}) {
103+
return function (serverPluginData) {
104+
return clientPluginData(password3, serverPluginData);
105+
};
106+
}
107+
}
108+
});
109+
```

lib/commands/change_user.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
// This file was modified by Oracle on September 21, 2021.
2+
// The changes involve saving additional authentication factor passwords
3+
// in the command scope and enabling multi-factor authentication in the
4+
// client-side when the server supports it.
5+
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
6+
17
'use strict';
28

39
const Command = require('./command.js');
410
const Packets = require('../packets/index.js');
11+
const ClientConstants = require('../constants/client');
512
const ClientHandshake = require('./client_handshake.js');
613
const CharsetToEncoding = require('../constants/charset_encodings.js');
714

@@ -11,10 +18,15 @@ class ChangeUser extends Command {
1118
this.onResult = callback;
1219
this.user = options.user;
1320
this.password = options.password;
21+
// "password1" is an alias of "password"
22+
this.password1 = options.password;
23+
this.password2 = options.password2;
24+
this.password3 = options.password3;
1425
this.database = options.database;
1526
this.passwordSha1 = options.passwordSha1;
1627
this.charsetNumber = options.charsetNumber;
1728
this.currentConfig = options.currentConfig;
29+
this.authenticationFactor = 0;
1830
}
1931
start(packet, connection) {
2032
const newPacket = new Packets.ChangeUser({
@@ -35,6 +47,13 @@ class ChangeUser extends Command {
3547
// reset prepared statements cache as all statements become invalid after changeUser
3648
connection._statements.reset();
3749
connection.writePacket(newPacket.toPacket());
50+
// check if the server supports multi-factor authentication
51+
const multiFactorAuthentication = connection.serverCapabilityFlags & ClientConstants.MULTI_FACTOR_AUTHENTICATION;
52+
if (multiFactorAuthentication) {
53+
// if the server supports multi-factor authentication, we enable it in
54+
// the client
55+
this.authenticationFactor = 1;
56+
}
3857
return ChangeUser.prototype.handshakeResult;
3958
}
4059
}

lib/commands/client_handshake.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
// emitted in the command instance itself.
44
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
55

6+
// This file was modified by Oracle on September 21, 2021.
7+
// Handshake workflow now supports additional authentication factors requested
8+
// by the server.
9+
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
10+
611
'use strict';
712

813
const Command = require('./command.js');
@@ -26,6 +31,7 @@ class ClientHandshake extends Command {
2631
super();
2732
this.handshake = null;
2833
this.clientFlags = clientFlags;
34+
this.authenticationFactor = 0;
2935
}
3036

3137
start() {
@@ -51,6 +57,14 @@ class ClientHandshake extends Command {
5157
}
5258
this.user = connection.config.user;
5359
this.password = connection.config.password;
60+
// "password1" is an alias to the original "password" value
61+
// to make it easier to integrate multi-factor authentication
62+
this.password1 = connection.config.password;
63+
// "password2" and "password3" are the 2nd and 3rd factor authentication
64+
// passwords, which can be undefined depending on the authentication
65+
// plugin being used
66+
this.password2 = connection.config.password2;
67+
this.password3 = connection.config.password3;
5468
this.passwordSha1 = connection.config.passwordSha1;
5569
this.database = connection.config.database;
5670
this.autPluginName = this.handshake.autPluginName;
@@ -109,6 +123,12 @@ class ClientHandshake extends Command {
109123
connection.connectionId = this.handshake.connectionId;
110124
const serverSSLSupport =
111125
this.handshake.capabilityFlags & ClientConstants.SSL;
126+
// multi factor authentication is enabled with the
127+
// "MULTI_FACTOR_AUTHENTICATION" capability and should only be used if it
128+
// is supported by the server
129+
const multiFactorAuthentication =
130+
this.handshake.capabilityFlags & ClientConstants.MULTI_FACTOR_AUTHENTICATION;
131+
this.clientFlags = this.clientFlags | multiFactorAuthentication;
112132
// use compression only if requested by client and supported by server
113133
connection.config.compress =
114134
connection.config.compress &&
@@ -141,17 +161,36 @@ class ClientHandshake extends Command {
141161
} else {
142162
this.sendCredentials(connection);
143163
}
164+
if (multiFactorAuthentication) {
165+
// if the server supports multi-factor authentication, we enable it in
166+
// the client
167+
this.authenticationFactor = 1;
168+
}
144169
return ClientHandshake.prototype.handshakeResult;
145170
}
146171

147172
handshakeResult(packet, connection) {
148173
const marker = packet.peekByte();
149-
if (marker === 0xfe || marker === 1) {
174+
// packet can be OK_Packet, ERR_Packet, AuthSwitchRequest, AuthNextFactor
175+
// or AuthMoreData
176+
if (marker === 0xfe || marker === 1 || marker === 0x02) {
150177
const authSwitch = require('./auth_switch');
151178
try {
152179
if (marker === 1) {
153180
authSwitch.authSwitchRequestMoreData(packet, connection, this);
154181
} else {
182+
// if authenticationFactor === 0, it means the server does not support
183+
// the multi-factor authentication capability
184+
if (this.authenticationFactor !== 0) {
185+
// if we are past the first authentication factor, we should use the
186+
// corresponding password (if there is one)
187+
connection.config.password = this[`password${this.authenticationFactor}`];
188+
// update the current authentication factor
189+
this.authenticationFactor += 1;
190+
}
191+
// if marker === 0x02, it means it is an AuthNextFactor packet,
192+
// which is similar in structure to an AuthSwitchRequest packet,
193+
// so, we can use it directly
155194
authSwitch.authSwitchRequest(packet, connection, this);
156195
}
157196
return ClientHandshake.prototype.handshakeResult;

lib/connection.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
// there is a fatal error.
99
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
1010

11+
// This file was modified by Oracle on September 21, 2021.
12+
// The changes involve passing additional authentication factor passwords
13+
// to the ChangeUser Command instance.
14+
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
15+
1116
'use strict';
1217

1318
const Net = require('net');
@@ -671,7 +676,12 @@ class Connection extends EventEmitter {
671676
new Commands.ChangeUser(
672677
{
673678
user: options.user || this.config.user,
674-
password: options.password || this.config.password,
679+
// for the purpose of multi-factor authentication, or not, the main
680+
// password (used for the 1st authentication factor) can also be
681+
// provided via the "password1" option
682+
password: options.password || options.password1 || this.config.password || this.config.password1,
683+
password2: options.password2 || this.config.password2,
684+
password3: options.password3 || this.config.password3,
675685
passwordSha1: options.passwordSha1 || this.config.passwordSha1,
676686
database: options.database || this.config.database,
677687
timeout: options.timeout,

lib/connection_config.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
// This file was modified by Oracle on September 21, 2021.
2+
// New connection options for additional authentication factors were
3+
// introduced.
4+
// Multi-factor authentication capability is now enabled if one of these
5+
// options is used.
6+
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
7+
18
'use strict';
29

310
const { URL } = require('url');
@@ -30,6 +37,11 @@ const validOptions = {
3037
namedPlaceholders: 1,
3138
nestTables: 1,
3239
password: 1,
40+
// with multi-factor authentication, the main password (used for the first
41+
// authentication factor) can be provided via password1
42+
password1: 1,
43+
password2: 1,
44+
password3: 1,
3345
passwordSha1: 1,
3446
pool: 1,
3547
port: 1,
@@ -81,7 +93,12 @@ class ConnectionConfig {
8193
this.localAddress = options.localAddress;
8294
this.socketPath = options.socketPath;
8395
this.user = options.user || undefined;
84-
this.password = options.password || undefined;
96+
// for the purpose of multi-factor authentication, or not, the main
97+
// password (used for the 1st authentication factor) can also be
98+
// provided via the "password1" option
99+
this.password = options.password || options.password1 || undefined;
100+
this.password2 = options.password2 || undefined;
101+
this.password3 = options.password3 || undefined;
85102
this.passwordSha1 = options.passwordSha1 || undefined;
86103
this.database = options.database;
87104
this.connectTimeout = isNaN(options.connectTimeout)

lib/constants/client.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
// This file was modified by Oracle on September 21, 2021.
2+
// New capability for multi-factor authentication based on mandatory session
3+
// trackers, that are signaled with an extra single-byte prefix on new
4+
// versions of the MySQL server.
5+
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
6+
17
'use strict';
28

39
// Manually extracted from mysql-5.5.23/include/mysql_com.h
@@ -29,3 +35,5 @@ exports.DEPRECATE_EOF = 0x01000000; /* Can send OK after a Text Resultset. */
2935

3036
exports.SSL_VERIFY_SERVER_CERT = 0x40000000;
3137
exports.REMEMBER_OPTIONS = 0x80000000;
38+
39+
exports.MULTI_FACTOR_AUTHENTICATION = 0x10000000; /* multi-factor authentication */

lib/packets/auth_next_factor.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) 2021, Oracle and/or its affiliates.
2+
3+
'use strict';
4+
5+
const Packet = require('../packets/packet');
6+
7+
class AuthNextFactor {
8+
constructor(opts) {
9+
this.pluginName = opts.pluginName;
10+
this.pluginData = opts.pluginData;
11+
}
12+
13+
toPacket(encoding) {
14+
const length = 6 + this.pluginName.length + this.pluginData.length;
15+
const buffer = Buffer.allocUnsafe(length);
16+
const packet = new Packet(0, buffer, 0, length);
17+
packet.offset = 4;
18+
packet.writeInt8(0x02);
19+
packet.writeNullTerminatedString(this.pluginName, encoding);
20+
packet.writeBuffer(this.pluginData);
21+
return packet;
22+
}
23+
24+
static fromPacket(packet, encoding) {
25+
packet.readInt8(); // marker
26+
const name = packet.readNullTerminatedString(encoding);
27+
const data = packet.readBuffer();
28+
return new AuthNextFactor({
29+
pluginName: name,
30+
pluginData: data
31+
});
32+
}
33+
}
34+
35+
module.exports = AuthNextFactor;

lib/packets/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
// binary server packet.
44
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
55

6+
// This file was modified by Oracle on September 21, 2021.
7+
// The new AuthNextFactor packet is now available.
8+
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
9+
610
'use strict';
711

812
const process = require('process');
913

14+
const AuthNextFactor = require('./auth_next_factor');
1015
const AuthSwitchRequest = require('./auth_switch_request');
1116
const AuthSwitchRequestMoreData = require('./auth_switch_request_more_data');
1217
const AuthSwitchResponse = require('./auth_switch_response');
@@ -27,6 +32,7 @@ const SSLRequest = require('./ssl_request');
2732
const TextRow = require('./text_row');
2833

2934
const ctorMap = {
35+
AuthNextFactor,
3036
AuthSwitchRequest,
3137
AuthSwitchRequestMoreData,
3238
AuthSwitchResponse,

0 commit comments

Comments
 (0)