Bug #93073 ContinueAuthentication only executes MoreData once
Submitted: 4 Nov 2018 12:06 Modified: 9 Nov 2022 15:49
Reporter: John Bayly (OCA) Email Updates:
Status: Not a Bug Impact on me:
None 
Category:Connector / NET Severity:S3 (Non-critical)
Version:8.0.13 OS:Any
Assigned to: CPU Architecture:Any
Tags: authentication, Contribution, NET, plugin

[4 Nov 2018 12:06] John Bayly
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);
[13 Nov 2018 14:46] OCA Admin
Contribution submitted via Github - Fix prefix check as per MySql bug report 93073 
(*) Contribution by John Bayly (Github tipstrade, mysql-connector-net/pull/26#issuecomment-437406587): I confirm the code being submitted is offered under the terms of the OCA, and that I am authorized to contribute it.

Contribution: git_patch_228992589.txt (text/plain), 1.26 KiB.

[17 Nov 2018 7:02] MySQL Verification Team
Hello John,

Thank you for the report and contribution.

regards,
Umesh
[9 Nov 2022 15:49] Daniel Valdez
In the process of authentication, the server could send a package of type AuthMoreData that means the authentication needs to continue; otherwise, it has ended. The structure of this package is as mentioned in the documentation (https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_p...) which states that the very first byte is 0x01, hence our condition.