Repository: sysown/proxysql (language: cpp)

Issue:
'Auth Method Switch' not being properly handled for COM_CHANGE_USER
- [x] A clear description of the issue

'Auth Method Switch' is behaving correctly for 'COM_CHANGE_USER' packet, leading to 'Access denied' errors in the client side when the 'COM_CHANGE_USER' is being issued by libmysqlclient library.

- [x] ProxySQL version

- v2.2.0

- [x] OS version

```
NAME="Arch Linux"
PRETTY_NAME="Arch Linux"
ID=arch
BUILD_ID=rolling
```

- [x] The steps to reproduce the issue

**Example program**: 
[change_user_bug.c.tar.gz](https://github.com/sysown/proxysql/files/6749410/change_user_bug.c.tar.gz)


Compile and execute the provided example program provided, when compiled against `libmariadb`, the program complete successfully, when compiled against `libmysqlclient`, the following error can be observed:

```
$ ./tests/change_user_bug
Setting charset utf8mb4
Server version: 5.5.30
Running query: SHOW VARIABLES LIKE '%coll%'
Result:
collation_connection = utf8mb4_unicode_ci
collation_database = utf8_general_ci
collation_server = utf8mb4_0900_ai_ci
default_collation_for_utf8mb4 = utf8mb4_0900_ai_ci
Failed to change user. Error: ProxySQL Error: Access denied for user 'root'@'127.0.0.1' (using password: YES)
```

##### Compilation steps

For compiling against `libmysql`:

```
g++ tests/change_user_bug.c -I/usr/include/mysql -lmysqlclient -o tests/change_user_bug
```

For compiling against `libmariadb` using ProxySQL dependencies:

```
g++ tests/change_user_bug.c -I../../deps/mariadb-client-library/mariadb_client/include -L../../deps/mariadb-client-library/mariadb_client/libmariadb -L../../deps/libssl/openssl/lib -lmariadbclient -ldl -lpthread -lssl -lcrypto -o tests/change_user_bug
```

Failure only occurs when compiling against `libmysql`.

- [x] The **full** ProxySQL error log (default location: `/var/lib/proxysql/proxysql.log`)

[proxysql_change_user_err.log](https://github.com/sysown/proxysql/files/6749353/proxysql_change_user_err.log)

##### ProxySQL configuration during test

```
mysql> select username,password,active from mysql_users;
+----------+----------+--------+
| username | password | active |
+----------+----------+--------+
| root     | root     | 1      |
| user     | user     | 1      |
| sbtest1  | sbtest1  | 1      |
+----------+----------+--------+
3 rows in set (0.00 sec)
```



'Auth Method Switch' not being properly handled for COM_CHANGE_USER
- [x] A clear description of the issue

'Auth Method Switch' is behaving correctly for 'COM_CHANGE_USER' packet, leading to 'Access denied' errors in the client side when the 'COM_CHANGE_USER' is being issued by libmysqlclient library.

- [x] ProxySQL version

- v2.2.0

- [x] OS version

```
NAME="Arch Linux"
PRETTY_NAME="Arch Linux"
ID=arch
BUILD_ID=rolling
```

- [x] The steps to reproduce the issue

**Example program**: 
[change_user_bug.c.tar.gz](https://github.com/sysown/proxysql/files/6749410/change_user_bug.c.tar.gz)


Compile and execute the provided example program provided, when compiled against `libmariadb`, the program complete successfully, when compiled against `libmysqlclient`, the following error can be observed:

```
$ ./tests/change_user_bug
Setting charset utf8mb4
Server version: 5.5.30
Running query: SHOW VARIABLES LIKE '%coll%'
Result:
collation_connection = utf8mb4_unicode_ci
collation_database = utf8_general_ci
collation_server = utf8mb4_0900_ai_ci
default_collation_for_utf8mb4 = utf8mb4_0900_ai_ci
Failed to change user. Error: ProxySQL Error: Access denied for user 'root'@'127.0.0.1' (using password: YES)
```

##### Compilation steps

For compiling against `libmysql`:

```
g++ tests/change_user_bug.c -I/usr/include/mysql -lmysqlclient -o tests/change_user_bug
```

For compiling against `libmariadb` using ProxySQL dependencies:

```
g++ tests/change_user_bug.c -I../../deps/mariadb-client-library/mariadb_client/include -L../../deps/mariadb-client-library/mariadb_client/libmariadb -L../../deps/libssl/openssl/lib -lmariadbclient -ldl -lpthread -lssl -lcrypto -o tests/change_user_bug
```

Failure only occurs when compiling against `libmysql`.

- [x] The **full** ProxySQL error log (default location: `/var/lib/proxysql/proxysql.log`)

[proxysql_change_user_err.log](https://github.com/sysown/proxysql/files/6749353/proxysql_change_user_err.log)

##### ProxySQL configuration during test

```
mysql> select username,password,active from mysql_users;
+----------+----------+--------+
| username | password | active |
+----------+----------+--------+
| root     | root     | 1      |
| user     | user     | 1      |
| sbtest1  | sbtest1  | 1      |
+----------+----------+--------+
3 rows in set (0.00 sec)
```





Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 617443c30a..539f686cff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -189,6 +189,8 @@ heaptrack.*
 test/tap/tests/galera_1_timeout_count
 test/tap/tests/galera_2_timeout_no_count
 test/tap/tests/setparser_test
+test/tap/tests/reg_test_3504-change_user_libmariadb_helper
+test/tap/tests/reg_test_3504-change_user_libmysql_helper
 test/tap/tests/generate_set_session_csv
 test/tap/tests/set_testing-240.csv
 local_testing_datadir/
diff --git a/include/MySQL_Protocol.h b/include/MySQL_Protocol.h
index 823b21263a..a7b68994c3 100644
--- a/include/MySQL_Protocol.h
+++ b/include/MySQL_Protocol.h
@@ -141,6 +141,31 @@ class MySQL_Protocol {
 	bool process_pkt_handshake_response(unsigned char *pkt, unsigned int len);
 	bool process_pkt_COM_CHANGE_USER(unsigned char *pkt, unsigned int len);
 	void * Query_String_to_packet(uint8_t sid, std::string *s, unsigned int *l);
+	/**
+	 * @brief Verifies the supplied 'user' and 'password' in order to authenticate an user. For
+	 *  doing so, it takes into account:
+	 *     * Current session type.
+	 *     * Current 'sha1' password for the user, reported by 'GloMyAuth' or 'GloClickHouseAuth'.
+	 *     * Current 'auth_plugin' being used for the session.
+	 *     * Username received sent by the client.
+	 *     * Password received sent by the client.
+	 *
+	 * @param session_type The session type inn which the authentication is taking place.
+	 * @param password Pointer to the stored password for the supplied user.
+	 * @param user Pointer to the user supplied by the client.
+	 * @param pass Pointer to the password supplied by the client.
+	 * @param pass_len Length of the supplied password received from the client.
+	 * @param sha1_pass Pointer to sha1_pass returned by auth cache.
+	 * @param auth_plugin Auth plugin supplied by client in the COM_CHANGE_USER packet. If
+	 *   the packet doesn't hold any, 'mysql_native_password' should be supplied
+	 *   as default.
+	 *
+	 * @details TODO: This function holds the same authentication block that can be seen in
+	 *   "MySQL_Protocol::process_pkt_handshake_response". That portion of the function should be
+	 *   refactored into using this very same function.
+	 * @return Returns 'true' if the user password was correctly verified, 'false' otherwise.
+	 */
+	bool verify_user_pass(enum proxysql_session_type session_type, const char* password, const char* user, const char* pass, int pass_len, const char* sha1_pass, const char* auth_plugin);
 
 	// prepared statements
 	bool generate_STMT_PREPARE_RESPONSE(uint8_t sequence_id, MySQL_STMT_Global_info *stmt_info, uint32_t _stmt_id=0);
diff --git a/include/MySQL_Session.h b/include/MySQL_Session.h
index 00094f7a44..ed9308cc3d 100644
--- a/include/MySQL_Session.h
+++ b/include/MySQL_Session.h
@@ -264,6 +264,16 @@ class MySQL_Session
 	bool session_fast_forward;
 	bool started_sending_data_to_client; // this status variable tracks if some result set was sent to the client, or if proxysql is still buffering everything
 	bool use_ssl;
+	/**
+	 * @brief This status variable tracks whether the session is performing an
+	 *   'Auth Switch' due to a 'COM_CHANGE_USER' packet.
+	 * @details It becomes 'true' when the packet is detected and processed by:
+	 *    - 'MySQL_Protocol::process_pkt_COM_CHANGE_USER'
+	 *   It's reset before sending the final response for 'Auth Switch' to the client by:
+	 *   -  'MySQL_Session::handler___status_CONNECTING_CLIENT___STATE_SERVER_HANDSHAKE'
+	 *   This flag was introduced for issue #3504.
+	 */
+	bool change_user_auth_switch;
 
 	bool with_gtid;
 
diff --git a/lib/MySQL_Protocol.cpp b/lib/MySQL_Protocol.cpp
index 4cf84fee10..88b2a795c7 100644
--- a/lib/MySQL_Protocol.cpp
+++ b/lib/MySQL_Protocol.cpp
@@ -1093,6 +1093,11 @@ bool MySQL_Protocol::generate_pkt_auth_switch_request(bool send, void **ptr, uns
 		myhdr.pkt_id++;
 	}
 
+	// Check if a 'COM_CHANGE_USER' Auth Switch is being performed in session
+	if ((*myds)->sess->change_user_auth_switch) {
+		myhdr.pkt_id=1;
+	}
+
 	switch((*myds)->switching_auth_type) {
 		case 1:
 			myhdr.pkt_length=1 // fe
@@ -1345,6 +1350,84 @@ bool MySQL_Protocol::process_pkt_auth_swich_response(unsigned char *pkt, unsigne
 	return ret;
 }
 
+bool MySQL_Protocol::verify_user_pass(
+	enum proxysql_session_type session_type,
+	const char* password,
+	const char* user,
+	const char* pass,
+	int pass_len,
+	const char* sha1_pass,
+	const char* auth_plugin
+) {
+	bool ret = false;
+
+	char reply[SHA_DIGEST_LENGTH+1];
+	reply[SHA_DIGEST_LENGTH]='\0';
+	int auth_plugin_id = 0;
+
+	if (strncmp((char *)auth_plugin,(char *)"mysql_native_password",strlen((char *)"mysql_native_password"))==0) {
+		auth_plugin_id = 1;
+	}
+	if (strncmp((char *)auth_plugin,(char *)"mysql_clear_password",strlen((char *)"mysql_clear_password"))==0) {
+		auth_plugin_id = 2;
+	}
+
+	if (password[0]!='*') { // clear text password
+		if (auth_plugin_id == 1) { // mysql_native_password
+			proxy_scramble(reply, (*myds)->myconn->scramble_buff, password);
+			if (memcmp(reply, pass, SHA_DIGEST_LENGTH)==0) {
+				ret=true;
+			}
+		} else { // mysql_clear_password
+			if (strncmp(password,(char *)pass,strlen(password))==0) {
+				ret=true;
+			}
+		}
+	} else {
+		if (auth_plugin_id == 1) {
+			if (session_type == PROXYSQL_SESSION_MYSQL || session_type == PROXYSQL_SESSION_SQLITE) {
+				ret=proxy_scramble_sha1((char *)pass,(*myds)->myconn->scramble_buff,password+1, reply);
+				if (ret) {
+					if (sha1_pass==NULL) {
+						GloMyAuth->set_SHA1((char *)user, USERNAME_FRONTEND,reply);
+					}
+					if (userinfo->sha1_pass) free(userinfo->sha1_pass);
+					userinfo->sha1_pass=sha1_pass_hex(reply);
+				}
+			}
+		} else {
+			if (session_type == PROXYSQL_SESSION_MYSQL || session_type == PROXYSQL_SESSION_SQLITE || session_type == PROXYSQL_SESSION_ADMIN || session_type == PROXYSQL_SESSION_STATS) {
+				proxy_debug(PROXY_DEBUG_MYSQL_AUTH, 5, "Session=%p , DS=%p , username='%s' , session_type=%d\n", (*myds), (*myds)->sess, user, session_type);
+				uint8_t hash_stage1[SHA_DIGEST_LENGTH];
+				uint8_t hash_stage2[SHA_DIGEST_LENGTH];
+				SHA_CTX sha1_context;
+				SHA1_Init(&sha1_context);
+				SHA1_Update(&sha1_context, pass, pass_len);
+				SHA1_Final(hash_stage1, &sha1_context);
+				SHA1_Init(&sha1_context);
+				SHA1_Update(&sha1_context,hash_stage1,SHA_DIGEST_LENGTH);
+				SHA1_Final(hash_stage2, &sha1_context);
+				// note that sha1_pass_hex() returns a new buffer
+				char *double_hashed_password = sha1_pass_hex((char *)hash_stage2);
+
+				if (strcasecmp(double_hashed_password,password)==0) {
+					ret = true;
+					if (sha1_pass==NULL) {
+						GloMyAuth->set_SHA1((char *)user, USERNAME_FRONTEND,hash_stage1);
+					}
+					if (userinfo->sha1_pass)
+						free(userinfo->sha1_pass);
+					userinfo->sha1_pass=sha1_pass_hex((char *)hash_stage1);
+				} else {
+					ret = false;
+				}
+				free(double_hashed_password);
+			}
+		}
+	}
+
+	return ret;
+}
 
 bool MySQL_Protocol::process_pkt_COM_CHANGE_USER(unsigned char *pkt, unsigned int len) {
 	bool ret=false;
@@ -1368,10 +1451,30 @@ bool MySQL_Protocol::process_pkt_COM_CHANGE_USER(unsigned char *pkt, unsigned in
 	memset(pass,0,128);
 	//pkt+=sizeof(mysql_hdr);
 	memcpy(pass, pkt+cur, pass_len);
-	char reply[SHA_DIGEST_LENGTH+1];
-	reply[SHA_DIGEST_LENGTH]='\0';
 	cur+=pass_len;
 	db=(char *)pkt+cur;
+	// Move to field after 'database'
+	cur += strlen(db) + 1;
+	// Skipt field 'character-set' (size 2)
+	cur += 2;
+	// Check and get 'Client Auth Plugin' if capability is supported
+	char* client_auth_plugin = nullptr;
+	if (pkt + len > pkt + cur) {
+		int capabilities = (*myds)->sess->client_myds->myconn->options.client_flag;
+		if (capabilities & CLIENT_PLUGIN_AUTH) {
+			client_auth_plugin = reinterpret_cast<char*>(pkt + cur);
+		}
+	}
+	// Default to 'mysql_native_password' in case 'auth_plugin' is not found.
+	if (client_auth_plugin == nullptr) {
+		client_auth_plugin = const_cast<char*>("mysql_native_password");
+	}
+	if (pass_len) {
+		if (pass[pass_len-1] == 0) {
+			pass_len--; // remove the extra 0 if present
+		}
+	}
+
 	void *sha1_pass=NULL;
 	enum proxysql_session_type session_type = (*myds)->sess->session_type;
 	if (session_type == PROXYSQL_SESSION_CLICKHOUSE) {
@@ -1391,23 +1494,24 @@ bool MySQL_Protocol::process_pkt_COM_CHANGE_USER(unsigned char *pkt, unsigned in
 		if (pass_len==0 && strlen(password)==0) {
 			ret=true;
 		} else {
-			if (password[0]!='*') { // clear text password
-				proxy_scramble(reply, (*myds)->myconn->scramble_buff, password);
-				if (memcmp(reply, pass, SHA_DIGEST_LENGTH)==0) {
-					ret=true;
-				}
+			// If pass not sent within 'COM_CHANGE_USER' packet, an 'Auth Switch Request'
+			// is required. We default to 'mysql_native_password'. See #3504 for more context.
+			if (pass_len == 0) {
+				// mysql_native_password
+				(*myds)->switching_auth_type = 1;
+				// started 'Auth Switch Request' for 'CHANGE_USER' in MySQL_Session.
+				(*myds)->sess->change_user_auth_switch = true;
+
+				generate_pkt_auth_switch_request(true, NULL, NULL);
+				(*myds)->myconn->userinfo->set((char *)user, NULL, db, NULL);
+				ret = false;
 			} else {
-				if (session_type == PROXYSQL_SESSION_MYSQL || session_type == PROXYSQL_SESSION_SQLITE) {
-					ret=proxy_scramble_sha1((char *)pass,(*myds)->myconn->scramble_buff,password+1, reply);
-					if (ret) {
-						if (sha1_pass==NULL) {
-							// currently proxysql doesn't know any sha1_pass for that specific user, let's set it!
-							GloMyAuth->set_SHA1((char *)user, USERNAME_FRONTEND,reply);
-						}
-						if (userinfo->sha1_pass) free(userinfo->sha1_pass);
-						userinfo->sha1_pass=sha1_pass_hex(reply);
-					}
-				}
+				// If pass is sent with 'COM_CHANGE_USER', we proceed trying to use
+				// it to authenticate the user. See #3504 for more context.
+				ret = verify_user_pass(
+					session_type, password, reinterpret_cast<char*>(user), reinterpret_cast<char*>(pass),
+					pass_len, static_cast<char*>(sha1_pass), client_auth_plugin
+				);
 			}
 		}
 		//if (_ret_use_ssl==true) {
diff --git a/lib/MySQL_Session.cpp b/lib/MySQL_Session.cpp
index c1934525f4..782c53581f 100644
--- a/lib/MySQL_Session.cpp
+++ b/lib/MySQL_Session.cpp
@@ -543,6 +543,7 @@ MySQL_Session::MySQL_Session() {
 
 	with_gtid = false;
 	use_ssl = false;
+	change_user_auth_switch = false;
 
 	//gtid_trxid = 0;
 	gtid_hid = -1;
@@ -1034,6 +1035,7 @@ void MySQL_Session::generate_proxysql_internal_session_json(json &j) {
 		}
 	}
 	j["client"]["DSS"] = client_myds->DSS;
+	j["client"]["switching_auth_type"] = client_myds->switching_auth_type;
 	j["default_schema"] = ( default_schema ? default_schema : "" );
 	j["user_attributes"] = ( user_attributes ? user_attributes : "" );
 	j["transaction_persistent"] = transaction_persistent;
@@ -5150,6 +5152,15 @@ void MySQL_Session::handler___status_CONNECTING_CLIENT___STATE_SERVER_HANDSHAKE(
 					uint8_t _pid = 2;
 					if (client_myds->switching_auth_stage) _pid+=2;
 					if (is_encrypted) _pid++;
+					// If this condition is met, it means that the
+					// 'STATE_SERVER_HANDSHAKE' being performed isn't from the start of a
+					// connection, but as a consequence of a 'COM_USER_CHANGE' which
+					// requires an 'Auth Switch'. Thus, we impose a 'pid' of '3' for the
+					// response 'OK' packet. See #3504 for more context.
+					if (change_user_auth_switch) {
+						_pid = 3;
+						change_user_auth_switch = 0;
+					}
 					if (use_ssl == true && is_encrypted == false) {
 						*wrong_pass=true;
 						GloMyLogger->log_audit_entry(PROXYSQL_MYSQL_AUTH_ERR, this, NULL);
@@ -6401,6 +6412,15 @@ void MySQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___MYSQL_C
 			// FIXME: max_connections is not handled for CHANGE_USER
 		} else {
 			l_free(pkt->size,pkt->ptr);
+			// 'COM_CHANGE_USER' didn't supply a password, and an 'Auth Switch Response' is
+			// required, going back to 'STATE_SERVER_HANDSHAKE' to perform the regular
+			// 'Auth Switch Response' for a connection is required. See #3504 for more context.
+			if (change_user_auth_switch) {
+				client_myds->DSS = STATE_SERVER_HANDSHAKE;
+				status = CONNECTING_CLIENT;
+				return;
+			}
+
 			proxy_debug(PROXY_DEBUG_MYSQL_CONNECTION, 5, "Wrong credentials for frontend: disconnecting\n");
 			*wrong_pass=true;
 		// FIXME: this should become close connection
@@ -6430,7 +6450,7 @@ void MySQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___MYSQL_C
 			}
 			char *_s=(char *)malloc(strlen(client_myds->myconn->userinfo->username)+100+strlen(client_addr));
 			sprintf(_s,"ProxySQL Error: Access denied for user '%s'@'%s' (using password: %s)", client_myds->myconn->userinfo->username, client_addr, (client_myds->myconn->userinfo->password ? "YES" : "NO"));
-			proxy_error("ProxySQL Error: Access denied for user '%s'@'%s' (using password: %s)", client_myds->myconn->userinfo->username, client_addr, (client_myds->myconn->userinfo->password ? "YES" : "NO"));
+			proxy_error("ProxySQL Error: Access denied for user '%s'@'%s' (using password: %s)\n", client_myds->myconn->userinfo->username, client_addr, (client_myds->myconn->userinfo->password ? "YES" : "NO"));
 			client_myds->myprot.generate_pkt_ERR(true,NULL,NULL,2,1045,(char *)"28000", _s, true);
 			free(_s);
 			__sync_fetch_and_add(&MyHGM->status.access_denied_wrong_password, 1);


Test patch:
diff --git a/test/tap/tap/utils.cpp b/test/tap/tap/utils.cpp
index fa5cd3d6f5..b465c11e0a 100644
--- a/test/tap/tap/utils.cpp
+++ b/test/tap/tap/utils.cpp
@@ -795,6 +795,7 @@ std::string tap_curtime() {
 	strftime(lut, 25, "%Y-%m-%d %H:%M:%S", &__tm_info);
 	std::string s = std::string(lut);
 	return s;
+}
 
 int get_proxysql_cpu_usage(const CommandLine& cl, uint32_t intv, uint32_t& cpu_usage) {
 	// check if proxysql process is consuming higher cpu than it should
diff --git a/test/tap/tap/utils.h b/test/tap/tap/utils.h
index f9af81b0e2..f83df178bc 100644
--- a/test/tap/tap/utils.h
+++ b/test/tap/tap/utils.h
@@ -177,8 +177,11 @@ int wait_for_replication(
  * NOTE: This is a duplicate of 'proxysql_find_charset_collate' in 'MySQL_Variables.h'. Including
  * 'MySQL_Variables' is not a easy task due to its interdependeces with other ProxySQL modules.
  */
+#ifdef LIBMYSQL_HELPER
+MY_CHARSET_INFO * proxysql_find_charset_collate(const char *collatename);
+#else
 MARIADB_CHARSET_INFO * proxysql_find_charset_collate(const char *collatename);
-
+#endif
 /**
  * @brief Creates the new supplied user in ProxySQL with the provided
  *   attributes.
diff --git a/test/tap/tests/Makefile b/test/tap/tests/Makefile
index 669b353661..1db7c1a937 100644
--- a/test/tap/tests/Makefile
+++ b/test/tap/tests/Makefile
@@ -101,7 +101,7 @@ OPT=-O2 $(WGCOV) -Wl,--no-as-needed
 debug: OPT=-O0 -DDEBUG -ggdb -Wl,--no-as-needed $(WGCOV) $(WASAN)
 debug: tests
 
-tests: $(patsubst %.cpp,%,$(wildcard *-t.cpp)) setparser_test set_testing-240.csv
+tests: $(patsubst %.cpp,%,$(wildcard *-t.cpp)) setparser_test reg_test_3504-change_user_libmariadb_helper reg_test_3504-change_user_libmysql_helper set_testing-240.csv
 testgalera: galera_1_timeout_count galera_2_timeout_no_count
 testaurora: aurora
 
@@ -143,3 +143,9 @@ create_connection_annotation: test_connection_annotation-t.cpp
 
 setparser_test: setparser_test.cpp $(TAP_LIBDIR)/libtap.a $(RE2_PATH)/util/test.cc $(LDIR)/set_parser.cpp $(LIBPROXYSQLAR)
 	g++ -DDEBUG setparser_test.cpp $(RE2_PATH)/util/test.cc ../../../src/obj/proxysql_global.o $(INCLUDEDIRS) $(LDIRS) $(OPT) -std=c++11 -lproxysql $(MYLIBS) -ltap -ldl -lpthread $(WASAN) -o setparser_test -DGITVERSION=\"$(GIT_VERSION)\"
+
+reg_test_3504-change_user_libmariadb_helper: reg_test_3504-change_user_helper.cpp
+	$(CXX) -DDEBUG reg_test_3504-change_user_helper.cpp $(INCLUDEDIRS) $(LDIRS) $(OPT) $(MYLIBS) -lpthread -ldl -std=c++11 -ltap $(STATIC_LIBS) -o reg_test_3504-change_user_libmariadb_helper -DGITVERSION=\"$(GIT_VERSION)\"
+
+reg_test_3504-change_user_libmysql_helper: reg_test_3504-change_user_helper.cpp
+	$(CXX) -DLIBMYSQL_HELPER -DDEBUG reg_test_3504-change_user_helper.cpp -I/usr/include/mysql -I$(IDIR) -I$(JSON_IDIR) -I../tap -L$(TAP_LIBDIR) -lpthread -ldl -std=c++11 -ltap -lmysqlclient -o reg_test_3504-change_user_libmysql_helper -DGITVERSION=\"$(GIT_VERSION)\"
diff --git a/test/tap/tests/reg_test_3504-change_user-t.cpp b/test/tap/tests/reg_test_3504-change_user-t.cpp
new file mode 100644
index 0000000000..fa9f09b902
--- /dev/null
+++ b/test/tap/tests/reg_test_3504-change_user-t.cpp
@@ -0,0 +1,197 @@
+/**
+ * @file reg_test_3504-change_user-t.cpp
+ * @brief This test checks the new implementation for 'COM_CHANGE_USER'
+ *   introduced in issue #3504. The test connects using different authentication
+ *   methods: 'mysql_clear_password', 'mysql_native_password' and
+ *   'caching_sha2_password', with and without SSL enabled. And verifies that both,
+ *   the initial connection and the later 'mysql_change_user' are properly executed.
+ *   Connections are performed using 'libmysqlclient' and 'libmariadb'.
+ * @details For making this possible the test uses two helper binaries, which
+ *   are the ones performing the connection to ProxySQL, and communicates to
+ *   them through a payload format that is specified in this helper tests files:
+ *     - 'reg_test_3504-change_user_libmysql_helper.cpp'
+ *     - 'reg_test_3504-change_user_libmariadb_helper.cpp'
+ */
+
+#include <cstring>
+#include <vector>
+#include <string>
+#include <stdio.h>
+#include <tuple>
+#include <iostream>
+#include <unistd.h>
+
+#include <mysql.h>
+#include <mysql/mysqld_error.h>
+
+#include "command_line.h"
+#include "proxysql_utils.h"
+#include "json.hpp"
+#include "tap.h"
+#include "utils.h"
+
+using nlohmann::json;
+
+using test_opts = std::tuple<std::string, bool, bool>;
+
+const std::vector<test_opts> tests_defs {
+	std::make_tuple("mysql_clear_password", false, false),
+	std::make_tuple("mysql_native_password", false, false),
+	std::make_tuple("caching_sha2_password", false, false),
+
+	std::make_tuple("mysql_clear_password", true, false),
+	std::make_tuple("mysql_native_password", true, false),
+	std::make_tuple("caching_sha2_password", true, false),
+
+	std::make_tuple("mysql_clear_password", false, true),
+	std::make_tuple("mysql_native_password", false, true),
+	std::make_tuple("caching_sha2_password", false, true),
+
+	std::make_tuple("mysql_clear_password", true, true),
+	std::make_tuple("mysql_native_password", true, true),
+	std::make_tuple("caching_sha2_password", true, true),
+};
+
+void perform_helper_test(
+	const std::string& helper_path,
+	const test_opts& test_opts
+) {
+	std::string result {};
+	std::string auth { std::get<0>(test_opts) };
+	bool exp_SSL_val = std::get<1>(test_opts);
+	bool change_user = std::get<2>(test_opts);
+
+	nlohmann::json input_json {};
+	input_json["user"] = "sbtest1";
+	input_json["pass"] = "sbtest1";
+	input_json["ch_user"] = "root";
+	input_json["ch_pass"] = "root";
+	input_json["auth"] = auth;
+	input_json["charset"] = "";
+	input_json["port"] = 6033;
+	input_json["SSL"] = exp_SSL_val;
+	input_json["CHANGE_USER"] = change_user;
+
+	std::string input_str { input_json.dump() };
+
+	std::vector<const char*> v_argv { helper_path.c_str(), input_str.c_str() };
+	int res = execvp(helper_path, v_argv, result);
+
+	diag("Result from helper, err_code: '%d', result: '%s'", res, result.c_str());
+
+	std::string err_msg {};
+	int exp_switching_auth_type = -1;
+	int act_switching_auth_type = 0;
+	std::string def_auth_plugin {};
+	bool act_SSL_val;
+	std::vector<std::string> exp_ch_usernames {};
+
+	if (change_user) {
+		exp_ch_usernames = { "root", "sbtest1", "root" };
+	} else {
+		exp_ch_usernames = { "sbtest1", "sbtest1", "sbtest1" };
+	}
+
+	std::vector<std::string> act_ch_usernames {};
+
+	try {
+		nlohmann::json output_res = nlohmann::json::parse(result);
+
+		if (output_res.contains("err_msg")) {
+			err_msg = output_res.at("err_msg");
+		}
+
+		act_switching_auth_type = output_res.at("switching_auth_type");
+		def_auth_plugin = output_res.at("def_auth_plugin");
+		act_SSL_val = output_res.at("ssl_enabled");
+
+		if (auth == "mysql_clear_password") {
+			exp_switching_auth_type = 0;
+		} else if  (auth == "mysql_native_password") {
+			exp_switching_auth_type = 0;
+		} else if (auth == "caching_sha2_password") {
+			exp_switching_auth_type = 1;
+		}
+
+		act_ch_usernames.push_back(output_res.at("client_com_change_user_1"));
+		act_ch_usernames.push_back(output_res.at("client_com_change_user_2"));
+		act_ch_usernames.push_back(output_res.at("client_com_change_user_3"));
+	} catch (const std::exception& ex) {
+		diag("Invalid JSON result from helper, parsing failed: '%s'", ex.what());
+	}
+
+	std::string exp_user_names_str =
+		std::accumulate(exp_ch_usernames.begin(), exp_ch_usernames.end(), std::string(),
+		[](const std::string& str, const std::string& splice) -> std::string {
+			return str + (str.length() > 0 ? "," : "") + splice;
+		});
+	std::string act_user_names_str =
+		std::accumulate(act_ch_usernames.begin(), act_ch_usernames.end(), std::string(),
+		[](const std::string& str, const std::string& splice) -> std::string {
+			return str + (str.length() > 0 ? "," : "") + splice;
+		});
+
+	ok(
+		(exp_switching_auth_type == act_switching_auth_type) &&
+		(exp_SSL_val == act_SSL_val) && err_msg.empty() &&
+		exp_ch_usernames == act_ch_usernames,
+		"Connect and COM_CHANGE_USER should work for the supplied values.\n"
+		" + Expected values where: (client_auth_plugin='%s', switching_auth_type='%d', SSL='%d', usernames=['%s']),\n"
+		" + Actual values where: (client_auth_plugin='%s', switching_auth_type='%d', SSL='%d, usernames=['%s']').\n"
+		" Error message: %s.\n",
+		auth.c_str(), exp_switching_auth_type, exp_SSL_val, exp_user_names_str.c_str(), def_auth_plugin.c_str(),
+		act_switching_auth_type, act_SSL_val, act_user_names_str.c_str(), err_msg.c_str()
+	);
+}
+
+int main(int argc, char** argv) {
+	CommandLine cl;
+
+	if (cl.getEnv()) {
+		diag("Failed to get the required environmental variables.");
+		return EXIT_FAILURE;
+	}
+
+	MYSQL* proxysql_admin = mysql_init(NULL);
+
+	if (
+		!mysql_real_connect(
+			proxysql_admin, "127.0.0.1", cl.admin_username, cl.admin_password,
+			"information_schema", cl.admin_port, NULL, 0
+		)
+	) {
+		fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxysql_admin));
+		return EXIT_FAILURE;
+	}
+
+	MYSQL_QUERY(proxysql_admin, "SET mysql-have_ssl='true'");
+	MYSQL_QUERY(proxysql_admin, "LOAD MYSQL VARIABLES TO RUNTIME");
+
+	// Give some time after the 'LOAD TO RUNTIME'
+	usleep(500 * 1000);
+
+	plan(tests_defs.size() * 2);
+
+	diag("Starting tests for helper 'reg_test_3504-change_user_libmysql_helper'\n");
+
+	std::string libmysql_helper_path {
+		std::string { cl.workdir } + "reg_test_3504-change_user_libmysql_helper"
+	};
+	for (const auto& test_opts : tests_defs) {
+		perform_helper_test(libmysql_helper_path, test_opts);
+	}
+
+	std::cout << "\n";
+	diag("Starting tests for helper 'reg_test_3504-change_user_libmariadb_helper'\n");
+
+	std::string libmariadb_helper_path {
+		std::string { cl.workdir } + "reg_test_3504-change_user_libmariadb_helper"
+	};
+	for (const auto& test_opts : tests_defs) {
+		perform_helper_test(libmariadb_helper_path, test_opts);
+	}
+
+	mysql_close(proxysql_admin);
+
+	return exit_status();
+}
diff --git a/test/tap/tests/reg_test_3504-change_user_helper.cpp b/test/tap/tests/reg_test_3504-change_user_helper.cpp
new file mode 100644
index 0000000000..1efb7a0c3f
--- /dev/null
+++ b/test/tap/tests/reg_test_3504-change_user_helper.cpp
@@ -0,0 +1,323 @@
+/**
+ * @file reg_test_3504-change_user_helper.cpp
+ * @brief This is a helper file to connect and execute a 'COM_CHANGE_USER' using
+ *   'libmariadb'/'libmysql' client library. The library election should be
+ *   performed by means of the macro 'LIBMYSQL_HELPER', when specified, the file
+ *   should be compiled against 'libmysql' library, when not, against 'libmariadb'.
+ *   It receives the inputs parameters for making the connection as a JSON from
+ *   the calling test and also returns it's output as a JSON.
+ *
+ *   Success JSON format:
+ *    {
+ *       "def_auth_plugin": Default auth plugin that was used by the
+ *         client for the connection.
+ *       "switching_auth_type": The 'switching_auth_type' that was required in the
+ *         connection, obtained via ProxySQL internal session.
+ *       "ssl_enabled": Confirmation that SSL is enabled in ProxySQL connection,
+ *         obtained via ProxySQL internal session.
+ *    }
+ *
+ *    Failure JSON format:
+ *     {
+ *        "err_msg": Error message holding the reason for the failed execution.
+ *     }
+ */
+
+#include <cstring>
+#include <vector>
+#include <string>
+#include <stdio.h>
+#include <numeric>
+#include <tuple>
+#include <iostream>
+
+#include <mysql.h>
+#include <mysql/mysqld_error.h>
+
+#include "proxysql_utils.h"
+#include "json.hpp"
+#include "tap.h"
+#include "utils.h"
+
+using nlohmann::json;
+
+using std::vector;
+using std::string;
+
+void parse_result_json_column(MYSQL_RES *result, json& j) {
+	if(!result) return;
+	MYSQL_ROW row;
+
+	while ((row = mysql_fetch_row(result))) {
+		j = json::parse(row[0]);
+	}
+}
+
+bool check_present_and_type(
+	const json& j, const std::vector<std::string>& path, const json::value_t& type
+) {
+	bool res = false;
+
+	json cur_j {};
+	cur_j = j;
+
+	for (const auto& step : path) {
+		bool cont_res = cur_j.contains(step);
+
+		if (cont_res) {
+			cur_j = cur_j.at(step);
+
+			if (&step == &path.back()) {
+				return type == cur_j.type();
+			}
+		} else {
+			break;
+		}
+	}
+
+	return res;
+}
+
+json extract_nested_elem(
+	const json& j, const std::vector<std::string>& path
+) {
+	json cur_j {};
+	cur_j = j;
+
+	for (const auto& step : path) {
+		if (cur_j.contains(step)) {
+			cur_j = cur_j.at(step);
+
+			if (&step == &path.back()) {
+				return cur_j;
+			}
+		} else {
+			break;
+		}
+	}
+
+	return cur_j;
+}
+
+
+int get_session_user_info(MYSQL* proxysql, std::string& user_info) {
+	int res = EXIT_FAILURE;
+
+	json j_status;
+	int query_res = mysql_query(proxysql, "PROXYSQL INTERNAL SESSION");
+	if (query_res) {
+		return query_res;
+	}
+
+	MYSQL_RES* tr_res = mysql_store_result(proxysql);
+	parse_result_json_column(tr_res, j_status);
+	mysql_free_result(tr_res);
+
+	std::string tmp_user_info {};
+	std::vector<std::string> info_path { "client", "userinfo", "username" };
+	json::value_t info_type = json::value_t::string;
+
+	if (check_present_and_type(j_status, info_path, info_type)) {
+		json j_user = extract_nested_elem(j_status, info_path);
+
+		if (!j_user.empty()) {
+			user_info = j_user.get<std::string>();
+			res = EXIT_SUCCESS;
+		}
+	}
+
+	return res;
+}
+
+int main(int argc, char** argv) {
+	nlohmann::json output {};
+	std::string err_msg {};
+	int res = EXIT_SUCCESS;
+
+	// Extract options
+	std::string user {};
+	std::string pass {};
+	std::string ch_user {};
+	std::string ch_pass {};
+	std::string auth {};
+	std::string charset {};
+	int         port;
+	bool        SSL;
+	bool        CHANGE_USER;
+
+	// MySQL handle
+	MYSQL mysql;
+	mysql_init(&mysql);
+
+	// Real 'AUTH' after connection
+	char* default_auth = nullptr;
+	// MySQL connection attempt result
+	MYSQL* conn_res = nullptr;
+
+	if (argc != 2) {
+		output["err_msg"] = "Invalid number of paramenters. Argc: '"
+			+ std::to_string(argc) + "'";
+		res = EXIT_FAILURE;
+		goto exit;
+	} else {
+		try {
+			nlohmann::json input = json::parse(argv[1]);
+
+			user = input.at("user");
+			pass = input.at("pass");
+			ch_user = input.at("ch_user");
+			ch_pass = input.at("ch_pass");
+			auth = input.at("auth");
+			charset = input.at("charset");
+			port = input.at("port");
+			SSL = input.at("SSL");
+			CHANGE_USER = input.at("CHANGE_USER");
+		} catch (std::exception& ex) {
+			output["err_msg"] =
+				std::string { "Exception while parsing input parameter: '" } +
+					ex.what() + "'";
+			res = EXIT_FAILURE;
+			goto exit;
+		}
+	}
+
+	// Default options if not set
+	if (port == 0) {
+		port = 6033;
+	}
+
+	// options
+	mysql_options(&mysql, MYSQL_DEFAULT_AUTH, auth.c_str());
+
+	if (auth == "mysql_clear_password") {
+		bool enable_cleartext = true;
+		mysql_options(&mysql, MYSQL_ENABLE_CLEARTEXT_PLUGIN, &enable_cleartext);
+	}
+
+	if (charset != "") {
+		mysql_options(&mysql, MYSQL_SET_CHARSET_NAME, "utf8mb4");
+	}
+
+#ifdef LIBMYSQL_HELPER
+	if (SSL == false) {
+		enum mysql_ssl_mode ssl_mode = SSL_MODE_DISABLED;
+		mysql_options(&mysql, MYSQL_OPT_SSL_MODE, &ssl_mode);
+	}
+
+	if (
+		!mysql_real_connect(
+			&mysql, "127.0.0.1", user.c_str(), pass.c_str(), "information_schema",
+			port, NULL, 0
+		)
+	) {
+		string_format(
+			"Failed to connect to database: Error: %s\n", err_msg,
+			mysql_error(&mysql)
+		);
+		output["err_msg"] = err_msg;
+		res = EXIT_FAILURE;
+
+		goto exit;
+	}
+#else
+	if (SSL == true) {
+		mysql_ssl_set(&mysql, NULL, NULL, NULL, NULL, NULL);
+		conn_res = mysql_real_connect(
+			&mysql, "127.0.0.1", user.c_str(), pass.c_str(), "information_schema",
+			port, NULL, CLIENT_SSL
+		);
+	} else {
+		conn_res = mysql_real_connect(
+			&mysql, "127.0.0.1", user.c_str(), pass.c_str(), "information_schema",
+			port, NULL, 0
+		);
+	}
+
+	if (!conn_res) {
+		string_format(
+			"Failed to connect to database: Error: %s\n", err_msg,
+			mysql_error(&mysql)
+		);
+		output["err_msg"] = err_msg;
+		res = EXIT_FAILURE;
+
+		goto exit;
+	}
+#endif
+
+	mysql_get_option(&mysql, MYSQL_DEFAULT_AUTH, &default_auth);
+	output["def_auth_plugin"] = std::string { default_auth };
+
+	{
+		json j_status;
+		MYSQL_QUERY(&mysql, "PROXYSQL INTERNAL SESSION");
+		MYSQL_RES* tr_res = mysql_store_result(&mysql);
+		parse_result_json_column(tr_res, j_status);
+		mysql_free_result(tr_res);
+
+		int auth_selected = -1;
+		bool ssl_enabled = false;
+
+		try {
+			auth_selected = j_status.at("client").at("switching_auth_type");
+			ssl_enabled = j_status.at("client").at("encrypted");
+		} catch (const std::exception& ex) {
+			string_format(
+				"Exception getting fields from 'PROXYSQL INTERNAL SESSION': '%s'",
+				err_msg, ex.what()
+			);
+			output["err_msg"] = err_msg;
+			res = EXIT_FAILURE;
+
+			goto exit;
+		}
+
+		output["switching_auth_type"] = auth_selected;
+		output["ssl_enabled"] = ssl_enabled;
+	}
+
+	{
+		const auto change_user_and_check = [&](const std::string user, const std::string pass, int num) -> int {
+			int tmp_res = EXIT_SUCCESS;
+
+			if (CHANGE_USER) {
+				if (mysql_change_user(&mysql, user.c_str(), pass.c_str(), "information_schema")) {
+					string_format(
+						"Failed to change user. Error: %s\n", err_msg, mysql_error(&mysql)
+					);
+					output["err_msg"] = err_msg;
+					tmp_res = EXIT_FAILURE;
+				}
+			}
+
+
+			if (tmp_res == EXIT_SUCCESS) {
+				std::string username {};
+				int info_err = get_session_user_info(&mysql, username);
+				if (info_err) {
+					output["err_msg"] = "Unable to get client user info from 'PROXYSQL INTERNAL SESSION'";
+					tmp_res = EXIT_FAILURE;
+				} else {
+					output["client_com_change_user_" + std::to_string(num)] = username;
+				}
+			}
+
+			return tmp_res;
+		};
+
+		/* Check: Change to first time user used in the connection */
+		if ((res=change_user_and_check(ch_user.c_str(), ch_pass.c_str(), 1))) { goto exit; }
+		/* Check: Already known user */
+		if ((res=change_user_and_check(user.c_str(), pass.c_str(), 2))) { goto exit; }
+		/* Check: Go back to already known user */
+		if ((res=change_user_and_check(ch_user.c_str(), ch_pass.c_str(), 3))) { goto exit; }
+	}
+
+
+exit:
+	mysql_close(&mysql);
+
+	std::cout << output.dump();
+
+	return res;
+}