Description:
When using 2-factor authentication, and a custom authentication plugin, MySqlAuthenticationPlugin.MoreData is called only once.
Currently, MySqlAuthenticationPlugin.ContinueAuthentication checks the Prefix byte of the packet, and returns if the prefix != 1.
If using pam authentication, which uses the "dialog" auth method, the prefix can also be 0x4. When this occurs ContinueAuthentication returns instead of passing the packet to MoreData.
Looking at the example C++ plugins that are built with the server, you'll notice that instead of checking prefix != 1, they check prefix == 0 || prefix == 254
See: https://github.com/mysql/mysql-server/blob/8.0/plugin/auth/test_plugin.cc
/* is it MySQL protocol (0=OK or 254=need old password) packet ? */
if (cmd == 0 || cmd == 254)
return CR_OK_HANDSHAKE_COMPLETE; /* yes. we're done */
How to repeat:
Configure 2FA PAM authentication for a user, using - for example - pam_google_authenticator.
Configure the client to use the following Authentication Plugin.
App.config:
<configSections>
<section name="MySQL" type="MySql.Data.MySqlClient.MySqlConfiguration, MySql.Data" />
</configSections>
<MySQL>
<AuthenticationPlugins>
<add name="dialog" type="MySQL_PAM.MySqlDialogAuthenticationPlugin, MySQL PAM"></add>
</AuthenticationPlugins>
</MySQL>
MySqlDialogAuthenticationPlugin.cs
using MySql.Data.MySqlClient.Authentication;
using System;
namespace MySQL_PAM {
public class MySqlDialogAuthenticationPlugin : MySqlAuthenticationPlugin {
public override string PluginName => "dialog";
public static event EventHandler<RequestValueEventArgs> OnRequestValue;
public override object GetPassword() => throw new NotImplementedException();
protected override byte[] MoreData(byte[] data) {
if (OnRequestValue == null) {
throw new InvalidOperationException($"{nameof(OnRequestValue)} must be handled.");
}
// When called first, data will be null as the message has already been stored by SetAuthData
var message = Encoding.GetString(data ?? AuthenticationData);
var e = new RequestValueEventArgs(message);
OnRequestValue.Invoke(this, e);
var passBytes = Encoding.GetBytes(e.Value ?? "");
var buffer = new byte[passBytes.Length + 1]; // Response must be null-terminated
Buffer.BlockCopy(passBytes, 0, buffer, 0, passBytes.Length);
return buffer;
}
protected override void SetAuthData(byte[] data) {
byte[] buffer = new byte[data.Length - 1]; // Remove the prefix from the Auth Method Data
Buffer.BlockCopy(data, 1, buffer, 0, buffer.Length);
base.SetAuthData(buffer);
}
public class RequestValueEventArgs : EventArgs {
public string Message { get; }
public string Value { get; set; }
public RequestValueEventArgs(string message) {
Message = message;
}
}
}
}
Suggested fix:
Change check in MySqlAuthenticationPlugin.ContinueAuthentication to mirror the checks in the C++ plugins.
--- MySQL.Data/src/Authentication/MySQLAuthenticationPlugin.cs.orig 2018-11-04 12:02:13.513946000 +0000
+++ MySQL.Data/src/Authentication/MySQLAuthenticationPlugin.cs 2018-11-04 12:03:29.358146000 +0000
@@ -247,10 +247,10 @@
_driver.SendPacket(packet);
packet = ReadPacket();
+ // Is it MySQL protocol (0=OK or 254=need old password) packet ?
byte prefixByte = packet.Buffer[0];
- if (prefixByte != 1) return;
+ if (prefixByte == 0 || prefixByte == 254) return;
- // A prefix of 0x01 means need more auth data.
byte[] responseData = new byte[packet.Length - 1];
Array.Copy(packet.Buffer, 1, responseData, 0, responseData.Length);
moreData = MoreData(responseData);