From b5754d3fb404641ac35c2601be4777e1db0890bb Mon Sep 17 00:00:00 2001
From: Vladimir Davydov <vdavydov@tarantool.org>
Date: Thu, 8 Dec 2022 15:37:53 +0300
Subject: [PATCH] box: make auth subsystem pluggable

This commit introduces an abstraction for the authentication code so
that one can easily add new methods. To add a new method, one just needs
to define a set of authentication callbacks in a struct auth_method and
register it with auth_method_register.

The IPROTO_AUTH and _user.auth formats were initially designed with
extensibility in mind: both take the authentication method name
(currently, only 'chap-sha1' is supported) so no changes to the schema
are required.

Note that although 'chap-sha1' is now implemented in its own file
src/box/auth_chap_sha1.c, we don't merge src/scramble.c into it.
This will be done later, in the scope of #7987.

Since we call authentication plug-ins "methods" (not "mechanisms"),
let's rename BOX_USER_FIELD_AUTH_MECH_LIST to BOX_USER_FIELD_AUTH while
we are at it. Anyway, the corresponding field of the _user system space
is called 'auth' (not 'auth_mech_list').

Closes #7986

NO_DOC=refactoring
NO_TEST=refactoring
NO_CHANGELOG=refactoring
---
 extra/exports                                 |   1 -
 src/box/CMakeLists.txt                        |   1 +
 src/box/alter.cc                              |  53 +++--
 src/box/applier.cc                            |  15 +-
 src/box/auth_chap_sha1.c                      | 186 +++++++++++++++++
 src/box/auth_chap_sha1.h                      |  23 ++
 src/box/authentication.c                      | 120 +++++++++--
 src/box/authentication.h                      | 196 ++++++++++++++++++
 src/box/box.cc                                |   2 +
 src/box/errcode.h                             |   3 +
 src/box/lua/misc.cc                           |  38 ++++
 src/box/lua/net_box.c                         |  30 +--
 src/box/lua/schema.lua                        |  27 +--
 src/box/schema_def.h                          |   2 +-
 src/box/user_def.c                            |   8 +-
 src/box/user_def.h                            |  17 +-
 src/box/xrow.c                                |  28 ++-
 src/box/xrow.h                                |  17 +-
 .../ghs_16_user_enumeration_test.lua          |  12 +-
 test/box/error.result                         |   3 +
 20 files changed, 658 insertions(+), 124 deletions(-)
 create mode 100644 src/box/auth_chap_sha1.c
 create mode 100644 src/box/auth_chap_sha1.h

diff --git a/extra/exports b/extra/exports
index ad930e0cde..5094a93caa 100644
--- a/extra/exports
+++ b/extra/exports
@@ -415,7 +415,6 @@ lua_upvaluejoin
 lua_xmove
 lua_yield
 memtx_tx_story_gc_step
-password_prepare
 PMurHash32
 PMurHash32_Process
 PMurHash32_Result
diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index 2c566f2066..7e98ea6646 100644
--- a/src/box/CMakeLists.txt
+++ b/src/box/CMakeLists.txt
@@ -206,6 +206,7 @@ set(box_sources
     user_def.c
     user.cc
     authentication.c
+    auth_chap_sha1.c
     replication.cc
     recovery.cc
     xstream.cc
diff --git a/src/box/alter.cc b/src/box/alter.cc
index 8370374224..5a290a2829 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -59,6 +59,7 @@
 #include "constraint_id.h"
 #include "space_upgrade.h"
 #include "box.h"
+#include "authentication.h"
 
 /* {{{ Auxiliary functions and methods. */
 
@@ -2883,11 +2884,9 @@ user_has_data(struct user *user, bool *has_data)
 }
 
 /**
- * Supposedly a user may have many authentication mechanisms
- * defined, but for now we only support chap-sha1. Get
- * password of chap-sha1 from the _user space.
+ * Initialize the user authenticator from the _user space data.
  */
-int
+static int
 user_def_fill_auth_data(struct user_def *user, const char *auth_data)
 {
 	uint8_t type = mp_typeof(*auth_data);
@@ -2908,35 +2907,36 @@ user_def_fill_auth_data(struct user_def *user, const char *auth_data)
 			  "use box.schema.user.passwd() to reset password");
 		return -1;
 	}
-	uint32_t mech_count = mp_decode_map(&auth_data);
-	for (uint32_t i = 0; i < mech_count; i++) {
+	uint32_t method_count = mp_decode_map(&auth_data);
+	for (uint32_t i = 0; i < method_count; i++) {
 		if (mp_typeof(*auth_data) != MP_STR) {
 			mp_next(&auth_data);
 			mp_next(&auth_data);
 			continue;
 		}
-		uint32_t len;
-		const char *mech_name = mp_decode_str(&auth_data, &len);
-		if (strncasecmp(mech_name, "chap-sha1", 9) != 0) {
-			mp_next(&auth_data);
+		uint32_t method_name_len;
+		const char *method_name = mp_decode_str(&auth_data,
+							&method_name_len);
+		const char *auth_data_end = auth_data;
+		mp_next(&auth_data_end);
+		const struct auth_method *method = auth_method_by_name(
+						method_name, method_name_len);
+		if (method == NULL) {
+			auth_data = auth_data_end;
 			continue;
 		}
-		const char *hash2_base64 = mp_decode_str(&auth_data, &len);
-		if (len != 0 && len != SCRAMBLE_BASE64_SIZE) {
-			diag_set(ClientError, ER_CREATE_USER,
-				  user->name, "invalid user password");
+		struct authenticator *auth = authenticator_new(
+				method, auth_data, auth_data_end);
+		if (auth == NULL)
+			return -1;
+		/* The guest user may only have an empty password. */
+		if (user->uid == GUEST &&
+		    !authenticate_password(auth, "", 0)) {
+			authenticator_delete(auth);
+			diag_set(ClientError, ER_GUEST_USER_PASSWORD);
 			return -1;
 		}
-		if (user->uid == GUEST) {
-			/** Guest user is permitted to have empty password */
-			if (strncmp(hash2_base64, CHAP_SHA1_EMPTY_PASSWORD, len)) {
-				diag_set(ClientError, ER_GUEST_USER_PASSWORD);
-				return -1;
-			}
-		}
-
-		base64_decode(hash2_base64, len, user->hash2,
-			      sizeof(user->hash2));
+		user->auth = auth;
 		break;
 	}
 	return 0;
@@ -2981,9 +2981,8 @@ user_def_new_from_tuple(struct tuple *tuple)
 	 * Check for trivial errors when a plain text
 	 * password is saved in this field instead.
 	 */
-	if (tuple_field_count(tuple) > BOX_USER_FIELD_AUTH_MECH_LIST) {
-		const char *auth_data =
-			tuple_field(tuple, BOX_USER_FIELD_AUTH_MECH_LIST);
+	if (tuple_field_count(tuple) > BOX_USER_FIELD_AUTH) {
+		const char *auth_data = tuple_field(tuple, BOX_USER_FIELD_AUTH);
 		const char *tmp = auth_data;
 		bool is_auth_empty;
 		if (mp_typeof(*auth_data) == MP_ARRAY &&
diff --git a/src/box/applier.cc b/src/box/applier.cc
index ea144296cf..69c3a3b529 100644
--- a/src/box/applier.cc
+++ b/src/box/applier.cc
@@ -32,6 +32,7 @@
 
 #include <msgpuck.h>
 
+#include "authentication.h"
 #include "xlog.h"
 #include "fiber.h"
 #include "fiber_cond.h"
@@ -429,11 +430,17 @@ applier_connect(struct applier *applier)
 
 	/* Authenticate */
 	applier_set_state(applier, APPLIER_AUTH);
+	const char *password = uri->password;
+	if (password == NULL)
+		password = "";
 	RegionGuard region_guard(&fiber()->gc);
-	xrow_encode_auth(&row, greeting.salt, greeting.salt_len, uri->login,
-			 strlen(uri->login),
-			 uri->password != NULL ? uri->password : "",
-			 uri->password != NULL ? strlen(uri->password) : 0);
+	const struct auth_method *method = AUTH_METHOD_DEFAULT;
+	const char *auth_request, *auth_request_end;
+	auth_request_prepare(method, password, strlen(password), greeting.salt,
+			     &auth_request, &auth_request_end);
+	xrow_encode_auth(&row, uri->login, strlen(uri->login),
+			 method->name, strlen(method->name),
+			 auth_request, auth_request_end);
 	coio_write_xrow(io, &row);
 	coio_read_xrow(io, ibuf, &row);
 	applier->last_row_time = ev_monotonic_now(loop());
diff --git a/src/box/auth_chap_sha1.c b/src/box/auth_chap_sha1.c
new file mode 100644
index 0000000000..a593c7ab3a
--- /dev/null
+++ b/src/box/auth_chap_sha1.c
@@ -0,0 +1,186 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright 2010-2022, Tarantool AUTHORS, please see AUTHORS file.
+ */
+#include "auth_chap_sha1.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#include "authentication.h"
+#include "base64.h"
+#include "diag.h"
+#include "errcode.h"
+#include "error.h"
+#include "fiber.h"
+#include "msgpuck.h"
+#include "scramble.h"
+#include "small/region.h"
+#include "trivia/util.h"
+
+#define AUTH_CHAP_SHA1_NAME "chap-sha1"
+
+/** chap-sha1 authenticator implementation. */
+struct auth_chap_sha1_authenticator {
+	/** Base class. */
+	struct authenticator base;
+	/** sha1(sha1(password)). */
+	char hash2[SCRAMBLE_SIZE];
+};
+
+/** auth_method::auth_method_delete */
+static void
+auth_chap_sha1_delete(struct auth_method *method)
+{
+	TRASH(method);
+	free(method);
+}
+
+/** auth_method::auth_data_prepare */
+static void
+auth_chap_sha1_data_prepare(const struct auth_method *method,
+			    const char *password, int password_len,
+			    const char **auth_data,
+			    const char **auth_data_end)
+{
+	(void)method;
+	struct region *region = &fiber()->gc;
+	size_t size = mp_sizeof_str(SCRAMBLE_BASE64_SIZE);
+	char *p = xregion_alloc(region, size);
+	*auth_data = p;
+	*auth_data_end = p + size;
+	p = mp_encode_strl(p, SCRAMBLE_BASE64_SIZE);
+	password_prepare(password, password_len, p, SCRAMBLE_BASE64_SIZE);
+}
+
+/** auth_method::auth_request_prepare */
+static void
+auth_chap_sha1_request_prepare(const struct auth_method *method,
+			       const char *password, int password_len,
+			       const char *salt,
+			       const char **auth_request,
+			       const char **auth_request_end)
+{
+	(void)method;
+	struct region *region = &fiber()->gc;
+	size_t size = mp_sizeof_str(SCRAMBLE_SIZE);
+	char *p = xregion_alloc(region, size);
+	*auth_request = p;
+	*auth_request_end = p + size;
+	p = mp_encode_strl(p, SCRAMBLE_SIZE);
+	scramble_prepare(p, salt, password, password_len);
+}
+
+/** auth_method::auth_request_check */
+static int
+auth_chap_sha1_request_check(const struct auth_method *method,
+			     const char *auth_request,
+			     const char *auth_request_end)
+{
+	(void)method;
+	uint32_t scramble_len;
+	if (mp_typeof(*auth_request) == MP_STR) {
+		scramble_len = mp_decode_strl(&auth_request);
+	} else if (mp_typeof(*auth_request) == MP_BIN) {
+		/*
+		 * Scramble is not a character stream, so some codecs
+		 * automatically pack it as MP_BIN.
+		 */
+		scramble_len = mp_decode_binl(&auth_request);
+	} else {
+		diag_set(ClientError, ER_INVALID_AUTH_REQUEST,
+			 AUTH_CHAP_SHA1_NAME, "scramble must be string");
+		return -1;
+	}
+	assert(auth_request + scramble_len == auth_request_end);
+	(void)auth_request_end;
+	if (scramble_len != SCRAMBLE_SIZE) {
+		diag_set(ClientError, ER_INVALID_AUTH_REQUEST,
+			 AUTH_CHAP_SHA1_NAME, "invalid scramble size");
+		return -1;
+	}
+	return 0;
+}
+
+/** auth_method::authenticator_new */
+static struct authenticator *
+auth_chap_sha1_authenticator_new(const struct auth_method *method,
+				 const char *auth_data,
+				 const char *auth_data_end)
+{
+	if (mp_typeof(*auth_data) != MP_STR) {
+		diag_set(ClientError, ER_INVALID_AUTH_DATA,
+			 AUTH_CHAP_SHA1_NAME, "scramble must be string");
+		return NULL;
+	}
+	uint32_t hash2_base64_len;
+	const char *hash2_base64 = mp_decode_str(&auth_data,
+						 &hash2_base64_len);
+	assert(auth_data == auth_data_end);
+	(void)auth_data_end;
+	if (hash2_base64_len != SCRAMBLE_BASE64_SIZE) {
+		diag_set(ClientError, ER_INVALID_AUTH_DATA,
+			 AUTH_CHAP_SHA1_NAME, "invalid scramble size");
+		return NULL;
+	}
+	struct auth_chap_sha1_authenticator *auth = xmalloc(sizeof(*auth));
+	auth->base.method = method;
+	int hash2_len = base64_decode(hash2_base64, hash2_base64_len,
+				      auth->hash2, sizeof(auth->hash2));
+	assert(hash2_len == sizeof(auth->hash2));
+	(void)hash2_len;
+	return (struct authenticator *)auth;
+}
+
+/** auth_method::authenticator_delete */
+static void
+auth_chap_sha1_authenticator_delete(struct authenticator *auth_)
+{
+	struct auth_chap_sha1_authenticator *auth =
+		(struct auth_chap_sha1_authenticator *)auth_;
+	TRASH(auth);
+	free(auth);
+}
+
+/** auth_method::authenticator_check_request */
+static bool
+auth_chap_sha1_authenticate_request(const struct authenticator *auth_,
+				    const char *salt,
+				    const char *auth_request,
+				    const char *auth_request_end)
+{
+	const struct auth_chap_sha1_authenticator *auth =
+		(const struct auth_chap_sha1_authenticator *)auth_;
+	uint32_t scramble_len;
+	const char *scramble;
+	if (mp_typeof(*auth_request) == MP_STR) {
+		scramble = mp_decode_str(&auth_request, &scramble_len);
+	} else if (mp_typeof(*auth_request) == MP_BIN) {
+		scramble = mp_decode_bin(&auth_request, &scramble_len);
+	} else {
+		unreachable();
+	}
+	assert(auth_request == auth_request_end);
+	(void)auth_request_end;
+	assert(scramble_len == SCRAMBLE_SIZE);
+	(void)scramble_len;
+	return scramble_check(scramble, salt, auth->hash2) == 0;
+}
+
+struct auth_method *
+auth_chap_sha1_new(void)
+{
+	struct auth_method *method = xmalloc(sizeof(*method));
+	method->name = AUTH_CHAP_SHA1_NAME;
+	method->auth_method_delete = auth_chap_sha1_delete;
+	method->auth_data_prepare = auth_chap_sha1_data_prepare;
+	method->auth_request_prepare = auth_chap_sha1_request_prepare;
+	method->auth_request_check = auth_chap_sha1_request_check;
+	method->authenticator_new = auth_chap_sha1_authenticator_new;
+	method->authenticator_delete = auth_chap_sha1_authenticator_delete;
+	method->authenticate_request = auth_chap_sha1_authenticate_request;
+	return method;
+}
diff --git a/src/box/auth_chap_sha1.h b/src/box/auth_chap_sha1.h
new file mode 100644
index 0000000000..9f1ec25155
--- /dev/null
+++ b/src/box/auth_chap_sha1.h
@@ -0,0 +1,23 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright 2010-2022, Tarantool AUTHORS, please see AUTHORS file.
+ */
+#pragma once
+
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+struct auth_method;
+
+/**
+ * Allocates and initializes 'chap-sha1' authentication method.
+ * This function never fails.
+ */
+struct auth_method *
+auth_chap_sha1_new(void);
+
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif /* defined(__cplusplus) */
diff --git a/src/box/authentication.c b/src/box/authentication.c
index ad330ecaf2..454b026c80 100644
--- a/src/box/authentication.c
+++ b/src/box/authentication.c
@@ -5,21 +5,54 @@
  */
 #include "authentication.h"
 
+#include <assert.h>
 #include <stdbool.h>
 #include <stdint.h>
 #include <stddef.h>
 #include <string.h>
 
+#include "assoc.h"
+#include "auth_chap_sha1.h"
 #include "base64.h"
 #include "diag.h"
 #include "errcode.h"
 #include "error.h"
+#include "fiber.h"
 #include "msgpuck.h"
 #include "scramble.h"
 #include "session.h"
+#include "small/region.h"
+#include "tt_static.h"
 #include "user.h"
 #include "user_def.h"
 
+const struct auth_method *AUTH_METHOD_DEFAULT;
+
+/** Map of all registered authentication methods: name -> auth_method. */
+static struct mh_strnptr_t *auth_methods = NULL;
+
+bool
+authenticate_password(const struct authenticator *auth,
+		      const char *password, int password_len)
+{
+	/*
+	 * We don't really need to zero the salt here, because any salt would
+	 * do as long as we use the same salt in auth_request_prepare and
+	 * authenticate_request. We zero it solely to avoid address sanitizer
+	 * complaints about usage of uninitialized memory.
+	 */
+	const char salt[SCRAMBLE_SIZE] = {0};
+	struct region *region = &fiber()->gc;
+	size_t region_svp = region_used(region);
+	const char *auth_request, *auth_request_end;
+	auth_request_prepare(auth->method, password, password_len, salt,
+			     &auth_request, &auth_request_end);
+	bool ret = authenticate_request(auth, salt, auth_request,
+					auth_request_end);
+	region_truncate(region, region_svp);
+	return ret;
+}
+
 int
 authenticate(const char *user_name, uint32_t user_name_len,
 	     const char *salt, const char *tuple)
@@ -37,46 +70,42 @@ authenticate(const char *user_name, uint32_t user_name_len,
 	 * to prevent user enumeration by analyzing error codes.
 	 */
 	diag_clear(diag_get());
-	uint32_t part_count;
-	uint32_t scramble_len;
-	const char *scramble;
 	/*
 	 * Allow authenticating back to the guest user without a password,
 	 * because the guest user isn't allowed to have a password, anyway.
 	 * This is useful for connection pooling.
 	 */
-	part_count = mp_decode_array(&tuple);
+	uint32_t part_count = mp_decode_array(&tuple);
 	if (part_count == 0 && user != NULL && user->def->uid == GUEST)
 		goto ok;
-
+	/* Expected: authentication method name and data. */
 	if (part_count < 2) {
-		/* Expected at least: authentication mechanism and data. */
 		diag_set(ClientError, ER_INVALID_MSGPACK,
 			 "authentication request body");
 		return -1;
 	}
-	mp_next(&tuple); /* Skip authentication mechanism. */
-	if (mp_typeof(*tuple) == MP_STR) {
-		scramble = mp_decode_str(&tuple, &scramble_len);
-	} else if (mp_typeof(*tuple) == MP_BIN) {
-		/*
-		 * scramble is not a character stream, so some
-		 * codecs automatically pack it as MP_BIN
-		 */
-		scramble = mp_decode_bin(&tuple, &scramble_len);
-	} else {
+	if (mp_typeof(*tuple) != MP_STR) {
 		diag_set(ClientError, ER_INVALID_MSGPACK,
-			 "authentication scramble");
+			 "authentication request body");
 		return -1;
 	}
-	if (scramble_len != SCRAMBLE_SIZE) {
-		/* Authentication mechanism, data. */
-		diag_set(ClientError, ER_INVALID_MSGPACK,
-			 "invalid scramble size");
+	uint32_t method_name_len;
+	const char *method_name = mp_decode_str(&tuple, &method_name_len);
+	const struct auth_method *method = auth_method_by_name(
+					method_name, method_name_len);
+	if (method == NULL) {
+		diag_set(ClientError, ER_UNKNOWN_AUTH_METHOD,
+			 tt_cstr(method_name, method_name_len));
 		return -1;
 	}
-	if (user == NULL ||
-	    scramble_check(scramble, salt, user->def->hash2) != 0) {
+	const char *auth_request = tuple;
+	const char *auth_request_end = tuple;
+	mp_next(&auth_request_end);
+	if (auth_request_check(method, auth_request, auth_request_end) != 0)
+		return -1;
+	if (user == NULL || user->def->auth == NULL ||
+	    !authenticate_request(user->def->auth, salt,
+				  auth_request, auth_request_end)) {
 		auth_res.is_authenticated = false;
 		if (session_run_on_auth_triggers(&auth_res) != 0)
 			return -1;
@@ -93,3 +122,48 @@ authenticate(const char *user_name, uint32_t user_name_len,
 	credentials_reset(&current_session()->credentials, user);
 	return 0;
 }
+
+const struct auth_method *
+auth_method_by_name(const char *name, uint32_t name_len)
+{
+	struct mh_strnptr_t *h = auth_methods;
+	mh_int_t i = mh_strnptr_find_str(h, name, name_len);
+	return i != mh_end(h) ? mh_strnptr_node(h, i)->val : NULL;
+}
+
+void
+auth_method_register(struct auth_method *method)
+{
+	struct mh_strnptr_t *h = auth_methods;
+	const char *name = method->name;
+	uint32_t name_len = strlen(name);
+	uint32_t name_hash = mh_strn_hash(name, name_len);
+	struct mh_strnptr_node_t n = {name, name_len, name_hash, method};
+	struct mh_strnptr_node_t prev;
+	struct mh_strnptr_node_t *prev_ptr = &prev;
+	mh_strnptr_put(h, &n, &prev_ptr, NULL);
+	assert(prev_ptr == NULL);
+}
+
+void
+auth_init(void)
+{
+	auth_methods = mh_strnptr_new();
+	struct auth_method *method = auth_chap_sha1_new();
+	AUTH_METHOD_DEFAULT = method;
+	auth_method_register(method);
+}
+
+void
+auth_free(void)
+{
+	struct mh_strnptr_t *h = auth_methods;
+	auth_methods = NULL;
+	AUTH_METHOD_DEFAULT = NULL;
+	mh_int_t i;
+	mh_foreach(h, i) {
+		struct auth_method *method = mh_strnptr_node(h, i)->val;
+		method->auth_method_delete(method);
+	}
+	mh_strnptr_delete(h);
+}
diff --git a/src/box/authentication.h b/src/box/authentication.h
index f645e31376..f64f87250c 100644
--- a/src/box/authentication.h
+++ b/src/box/authentication.h
@@ -5,6 +5,7 @@
  */
 #pragma once
 
+#include <assert.h>
 #include <stdbool.h>
 #include <stddef.h>
 #include <stdint.h>
@@ -13,6 +14,11 @@
 extern "C" {
 #endif /* defined(__cplusplus) */
 
+struct auth_method;
+
+/** Default authentication method. */
+extern const struct auth_method *AUTH_METHOD_DEFAULT;
+
 /**
  * State passed to authentication trigger.
  */
@@ -25,6 +31,170 @@ struct on_auth_trigger_ctx {
 	bool is_authenticated;
 };
 
+/**
+ * Abstract authenticator class.
+ *
+ * A concrete instance of this class is created for each user that
+ * enabled authentication.
+ */
+struct authenticator {
+	/** Authentication method used by this authenticator. */
+	const struct auth_method *method;
+};
+
+/**
+ * Abstract authentication method class.
+ *
+ * A concrete instance of this class is created for each supported
+ * authentication method.
+ */
+struct auth_method {
+	/** Unique authentication method name. */
+	const char *name;
+	/** Destroys an authentication method object. */
+	void
+	(*auth_method_delete)(struct auth_method *method);
+	/**
+	 * Given a password, this function prepares MsgPack data that can be
+	 * used to check an authentication request. The data is allocated on
+	 * the fiber region.
+	 *
+	 * We store the data as a value in the 'auth' field of the '_user'
+	 * system table, using the authentication method name as a key.
+	 */
+	void
+	(*auth_data_prepare)(const struct auth_method *method,
+			     const char *password, int password_len,
+			     const char **auth_data,
+			     const char **auth_data_end);
+	/**
+	 * Given a password and a connection salt (sent in the greeting),
+	 * this function prepares MsgPack data that can be used to perform
+	 * an authentication request. The data is allocated on the fiber
+	 * region.
+	 *
+	 * We store the data in the second field of IPROTO_TUPLE sent in
+	 * IPROTO_AUTH request body. The first IPROTO_TUPLE field is set to
+	 * the authentication method name.
+	 */
+	void
+	(*auth_request_prepare)(const struct auth_method *method,
+				const char *password, int password_len,
+				const char *salt,
+				const char **auth_request,
+				const char **auth_request_end);
+	/**
+	 * Checks the format of an authentication request.
+	 *
+	 * Returns 0 on success. If the request is malformed, sets diag to
+	 * ER_INVALID_AUTH_REQUEST and returns -1.
+	 *
+	 * See also auth_request_prepare.
+	 */
+	int
+	(*auth_request_check)(const struct auth_method *method,
+			      const char *auth_request,
+			      const char *auth_request_end);
+	/**
+	 * Constructs a new authenticator object from an authentication data.
+	 *
+	 * Returns a pointer on success. If the data is malformed, sets diag to
+	 * ER_INVALID_AUTH_DATA and returns NULL.
+	 *
+	 * See also auth_data_prepare.
+	 */
+	struct authenticator *
+	(*authenticator_new)(const struct auth_method *method,
+			     const char *auth_data, const char *auth_data_end);
+	/** Destroys an authenticator object. */
+	void
+	(*authenticator_delete)(struct authenticator *auth);
+	/**
+	 * Authenticates a request.
+	 *
+	 * Returns true if authentication was successful.
+	 *
+	 * The request must be well-formed.
+	 * The salt must match the salt used to prepare the request.
+	 *
+	 * See also auth_request_prepare, auth_request_check.
+	 */
+	bool
+	(*authenticate_request)(const struct authenticator *auth,
+				const char *salt,
+				const char *auth_request,
+				const char *auth_request_end);
+};
+
+static inline void
+auth_data_prepare(const struct auth_method *method,
+		  const char *password, int password_len,
+		  const char **auth_data, const char **auth_data_end)
+{
+	method->auth_data_prepare(method, password, password_len,
+				 auth_data, auth_data_end);
+}
+
+static inline void
+auth_request_prepare(const struct auth_method *method,
+		     const char *password, int password_len, const char *salt,
+		     const char **auth_request, const char **auth_request_end)
+{
+	method->auth_request_prepare(method, password, password_len, salt,
+				     auth_request, auth_request_end);
+}
+
+static inline int
+auth_request_check(const struct auth_method *method,
+		   const char *auth_request, const char *auth_request_end)
+{
+	return method->auth_request_check(method, auth_request,
+					  auth_request_end);
+}
+
+static inline struct authenticator *
+authenticator_new(const struct auth_method *method,
+		  const char *auth_data, const char *auth_data_end)
+{
+	return method->authenticator_new(method, auth_data, auth_data_end);
+}
+
+static inline void
+authenticator_delete(struct authenticator *auth)
+{
+	auth->method->authenticator_delete(auth);
+}
+
+/**
+ * Authenticates a request.
+ *
+ * Returns true if authentication was successful.
+ *
+ * NOTE: the request must be well-formed (checked by auth_request_check).
+ */
+static inline bool
+authenticate_request(const struct authenticator *auth, const char *salt,
+		     const char *auth_request, const char *auth_request_end)
+{
+	assert(auth->method->auth_request_check(auth->method, auth_request,
+						auth_request_end) == 0);
+	return auth->method->authenticate_request(
+			auth, salt, auth_request, auth_request_end);
+}
+
+/**
+ * Authenticates a password.
+ *
+ * Returns true if authentication was successful.
+ *
+ * This is a helper function that prepares an authentication request with
+ * auth_request_prepare and then checks it with authenticate_request using
+ * zero salt.
+ */
+bool
+authenticate_password(const struct authenticator *auth,
+		      const char *password, int password_len);
+
 /**
  * Authenticates a user.
  *
@@ -35,11 +205,37 @@ struct on_auth_trigger_ctx {
  * tuple: value of the IPROTO_TUPLE key sent in the IPROTO_AUTH request body.
  *
  * Returns 0 on success. On error, sets diag and returns -1.
+ *
+ * Errors:
+ *   ER_INVALID_MSGPACK: missing authentication method name or data
+ *   ER_UNKNOWN_AUTH_METHOD: unknown authentication method name
+ *   ER_INVALID_AUTH_REQUEST: malformed authentication request
+ *   ER_CREDS_MISMATCH: authentication denied
  */
 int
 authenticate(const char *user_name, uint32_t user_name_len,
 	     const char *salt, const char *tuple);
 
+/**
+ * Looks up an authentication method by name.
+ * If not found, returns NULL (diag NOT set).
+ */
+const struct auth_method *
+auth_method_by_name(const char *name, uint32_t name_len);
+
+/**
+ * Registers an authentication method.
+ * There must not be another method with the same name.
+ */
+void
+auth_method_register(struct auth_method *method);
+
+void
+auth_init(void);
+
+void
+auth_free(void);
+
 #if defined(__cplusplus)
 } /* extern "C" */
 #endif /* defined(__cplusplus) */
diff --git a/src/box/box.cc b/src/box/box.cc
index 490d9826bb..461e16865f 100644
--- a/src/box/box.cc
+++ b/src/box/box.cc
@@ -3739,6 +3739,7 @@ box_free(void)
 		schema_module_free();
 		tuple_free();
 #endif
+		auth_free();
 		wal_ext_free();
 		box_watcher_free();
 		box_raft_free();
@@ -4285,6 +4286,7 @@ box_init(void)
 	msgpack_init();
 	fiber_cond_create(&ro_cond);
 
+	auth_init();
 	user_cache_init();
 	/*
 	 * The order is important: to initialize sessions,
diff --git a/src/box/errcode.h b/src/box/errcode.h
index 9362b8ee58..f093b018e2 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -301,6 +301,9 @@ struct errcode_record {
 	/*246 */_(ER_INTERFERING_ELECTIONS,	"Interfering elections started")\
 	/*247 */_(ER_ITERATOR_POSITION,		"Iterator position is invalid") \
 	/*248 */_(ER_DDL_NOT_ALLOWED,		"DDL operations are not allowed: %s") \
+	/*249 */_(ER_UNKNOWN_AUTH_METHOD,	"Unknown authentication method '%s'") \
+	/*250 */_(ER_INVALID_AUTH_DATA,		"Invalid '%s' data: %s") \
+	/*251 */_(ER_INVALID_AUTH_REQUEST,	"Invalid '%s' request: %s") \
 
 /*
  * !IMPORTANT! Please follow instructions at start of the file
diff --git a/src/box/lua/misc.cc b/src/box/lua/misc.cc
index 5915193086..720267e438 100644
--- a/src/box/lua/misc.cc
+++ b/src/box/lua/misc.cc
@@ -35,6 +35,7 @@
 #include "lua/utils.h"
 #include "lua/msgpack.h"
 
+#include "box/authentication.h"
 #include "box/box.h"
 #include "box/port.h"
 #include "box/tuple.h"
@@ -43,6 +44,7 @@
 #include "box/xrow.h"
 #include "box/txn.h"
 #include "mpstream/mpstream.h"
+#include "tt_static.h"
 
 static uint32_t CTID_STRUCT_TUPLE_FORMAT_PTR;
 
@@ -204,6 +206,41 @@ port_msgpack_dump_lua(struct port *base, struct lua_State *L, bool is_flat)
 
 /* }}} */
 
+/** {{{ Helper that generates user auth data. **/
+
+/**
+ * Takes authentication method name (e.g. 'chap-sha1') and a password.
+ * Returns authentication data that can be stored in the _user space.
+ * Raises Lua error if the specified authentication method doesn't exist.
+ */
+static int
+lbox_prepare_auth(lua_State *L)
+{
+	size_t method_name_len;
+	const char *method_name = luaL_checklstring(L, 1, &method_name_len);
+	size_t password_len;
+	const char *password = luaL_checklstring(L, 2, &password_len);
+	const struct auth_method *method = auth_method_by_name(method_name,
+							       method_name_len);
+	if (method == NULL) {
+		diag_set(ClientError, ER_UNKNOWN_AUTH_METHOD,
+			 tt_cstr(method_name, method_name_len));
+		return luaT_error(L);
+	}
+	struct region *region = &fiber()->gc;
+	size_t region_svp = region_used(region);
+	const char *auth_data, *auth_data_end;
+	auth_data_prepare(method, password, password_len,
+			  &auth_data, &auth_data_end);
+	luamp_decode(L, luaL_msgpack_default, &auth_data);
+	assert(auth_data == auth_data_end);
+	(void)auth_data_end;
+	region_truncate(region, region_svp);
+	return 1;
+}
+
+/* }}} */
+
 /** {{{ Lua/C implementation of index:select(): used only by Vinyl **/
 
 static int
@@ -388,6 +425,7 @@ void
 box_lua_misc_init(struct lua_State *L)
 {
 	static const struct luaL_Reg boxlib_internal[] = {
+		{"prepare_auth", lbox_prepare_auth},
 		{"select", lbox_select},
 		{"new_tuple_format", lbox_tuple_format_new},
 		{"txn_set_isolation", lbox_txn_set_isolation},
diff --git a/src/box/lua/net_box.c b/src/box/lua/net_box.c
index e08521ff6e..e95523c2ce 100644
--- a/src/box/lua/net_box.c
+++ b/src/box/lua/net_box.c
@@ -38,10 +38,8 @@
 #include <sys/socket.h>
 #include <sys/types.h>
 
-#include <small/ibuf.h>
-#include <msgpuck.h> /* mp_store_u32() */
-#include "scramble.h"
-
+#include "box/authentication.h"
+#include "box/errcode.h"
 #include "box/iproto_constants.h"
 #include "box/iproto_features.h"
 #include "box/lua/tuple.h" /* luamp_convert_tuple() / luamp_convert_key() */
@@ -51,18 +49,18 @@
 #include "box/error.h"
 #include "box/schema_def.h"
 
-#include "lua/msgpack.h"
-#include <base64.h>
-
 #include "assoc.h"
 #include "coio.h"
 #include "fiber.h"
 #include "fiber_cond.h"
 #include "iostream.h"
-#include "box/errcode.h"
 #include "lua/fiber.h"
 #include "lua/fiber_cond.h"
+#include "lua/msgpack.h"
 #include "lua/uri.h"
+#include "msgpuck.h"
+#include "small/ibuf.h"
+#include "small/region.h"
 #include "mpstream/mpstream.h"
 #include "misc.h" /* lbox_check_tuple_format() */
 #include "uri/uri.h"
@@ -687,9 +685,14 @@ static void
 netbox_encode_auth(struct lua_State *L, struct ibuf *ibuf, uint64_t sync,
 		   const char *user, const char *password, const char *salt)
 {
-	char scramble[SCRAMBLE_SIZE];
-	scramble_prepare(scramble, salt, password != NULL ? password : "",
-			 password != NULL ? strlen(password) : 0);
+	if (password == NULL)
+		password = "";
+	struct region *region = &fiber()->gc;
+	size_t region_svp = region_used(region);
+	const struct auth_method *method = AUTH_METHOD_DEFAULT;
+	const char *auth_request, *auth_request_end;
+	auth_request_prepare(method, password, strlen(password), salt,
+			     &auth_request, &auth_request_end);
 	struct mpstream stream;
 	mpstream_init(&stream, ibuf, ibuf_reserve_cb, ibuf_alloc_cb,
 		      luamp_error, L);
@@ -699,9 +702,10 @@ netbox_encode_auth(struct lua_State *L, struct ibuf *ibuf, uint64_t sync,
 	mpstream_encode_strn(&stream, user, strlen(user));
 	mpstream_encode_uint(&stream, IPROTO_TUPLE);
 	mpstream_encode_array(&stream, 2);
-	mpstream_encode_str(&stream, "chap-sha1");
-	mpstream_encode_strn(&stream, scramble, SCRAMBLE_SIZE);
+	mpstream_encode_str(&stream, method->name);
+	mpstream_memcpy(&stream, auth_request, auth_request_end - auth_request);
 	netbox_end_encode(&stream, svp);
+	region_truncate(region, region_svp);
 }
 
 /**
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index 1a9ecc972c..0f4cc7bfb1 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -121,9 +121,6 @@ ffi.cdef[[
                const char **after, const char **after_end,
                bool update_pos, struct port *port);
 
-    void password_prepare(const char *password, int len,
-                          char *out, int out_len);
-
     enum priv_type {
         PRIV_R = 1,
         PRIV_W = 2,
@@ -3376,20 +3373,16 @@ end
 box.schema.user = {}
 
 box.schema.user.password = function(password)
-    local BUF_SIZE = 128
-    local ibuf = cord_ibuf_take()
-    local buf = ibuf:alloc(BUF_SIZE)
-    builtin.password_prepare(password, #password, buf, BUF_SIZE)
-    buf = ffi.string(buf)
-    cord_ibuf_put(ibuf)
-    return buf
+    return internal.prepare_auth('chap-sha1', password)
+end
+
+local function prepare_auth_list(password)
+    return {['chap-sha1'] = internal.prepare_auth('chap-sha1', password)}
 end
 
 local function chpasswd(uid, new_password)
     local _user = box.space[box.schema.USER_ID]
-    local auth_mech_list = {}
-    auth_mech_list["chap-sha1"] = box.schema.user.password(new_password)
-    _user:update({uid}, {{"=", 5, auth_mech_list}})
+    _user:update({uid}, {{"=", 5, prepare_auth_list(new_password)}})
 end
 
 box.schema.user.passwd = function(name, new_password)
@@ -3420,12 +3413,14 @@ box.schema.user.create = function(name, opts)
         end
         return
     end
-    local auth_mech_list = setmap({})
+    local auth_list
     if opts.password then
-        auth_mech_list["chap-sha1"] = box.schema.user.password(opts.password)
+        auth_list = prepare_auth_list(opts.password)
+    else
+        auth_list = setmap({})
     end
     local _user = box.space[box.schema.USER_ID]
-    uid = _user:auto_increment{session.euid(), name, 'user', auth_mech_list}.id
+    uid = _user:auto_increment{session.euid(), name, 'user', auth_list}.id
     -- grant role 'public' to the user
     box.schema.user.grant(uid, 'public')
     -- Grant privilege 'alter' on itself, so that it can
diff --git a/src/box/schema_def.h b/src/box/schema_def.h
index 21da0d668c..cf7fc7ece8 100644
--- a/src/box/schema_def.h
+++ b/src/box/schema_def.h
@@ -153,7 +153,7 @@ enum {
 	BOX_USER_FIELD_UID = 1,
 	BOX_USER_FIELD_NAME = 2,
 	BOX_USER_FIELD_TYPE = 3,
-	BOX_USER_FIELD_AUTH_MECH_LIST = 4,
+	BOX_USER_FIELD_AUTH = 4,
 };
 
 /** _priv fields. */
diff --git a/src/box/user_def.c b/src/box/user_def.c
index c9b7452ace..db3d719861 100644
--- a/src/box/user_def.c
+++ b/src/box/user_def.c
@@ -8,13 +8,11 @@
 #include <assert.h>
 #include <stdint.h>
 #include <stdlib.h>
-#include <string.h>
 
+#include "authentication.h"
 #include "salad/grp_alloc.h"
 #include "trivia/util.h"
 
-const char *CHAP_SHA1_EMPTY_PASSWORD = "vhvewKp0tNyweZQ+cFKAlsyphfg=";
-
 const char *
 priv_name(user_access_t access)
 {
@@ -54,7 +52,7 @@ user_def_new(uint32_t uid, uint32_t owner, enum schema_object_type type,
 	def->uid = uid;
 	def->owner = owner;
 	def->type = type;
-	memset(def->hash2, 0, sizeof(def->hash2));
+	def->auth = NULL;
 	def->name = grp_alloc_create_str(&all, name, name_len);
 	assert(grp_alloc_size(&all) == 0);
 	return def;
@@ -63,6 +61,8 @@ user_def_new(uint32_t uid, uint32_t owner, enum schema_object_type type,
 void
 user_def_delete(struct user_def *def)
 {
+	if (def->auth != NULL)
+		authenticator_delete(def->auth);
 	TRASH(def);
 	free(def);
 }
diff --git a/src/box/user_def.h b/src/box/user_def.h
index ea29590228..efeb9774cd 100644
--- a/src/box/user_def.h
+++ b/src/box/user_def.h
@@ -8,7 +8,6 @@
 #include <stdint.h>
 
 #include "schema_def.h" /* for SCHEMA_OBJECT_TYPE */
-#include "scramble.h" /* for SCRAMBLE_SIZE */
 #define RB_COMPACT 1
 #include "small/rb.h"
 #include "small/rlist.h"
@@ -17,10 +16,7 @@
 extern "C" {
 #endif /* defined(__cplusplus) */
 
-/**
- * chap-sha1 of empty string, i.e. base64_encode(sha1(sha1(""), 0)
- */
-extern const char *CHAP_SHA1_EMPTY_PASSWORD;
+struct authenticator;
 
 typedef uint16_t user_access_t;
 /**
@@ -141,8 +137,15 @@ struct user_def {
 	uint32_t owner;
 	/** 'user' or 'role' */
 	enum schema_object_type type;
-	/** User password - hash2 */
-	char hash2[SCRAMBLE_SIZE];
+	/**
+	 * Authentication data or NULL if auth method is unset.
+	 *
+	 * XXX: Strictly speaking, this doesn't belong here.
+	 * Ideally, we should store raw authentication data in
+	 * the user_def struct while the authenticator should
+	 * reside in the user struct.
+	 */
+	struct authenticator *auth;
 	/** User name - for error messages and debugging */
 	char *name;
 };
diff --git a/src/box/xrow.c b/src/box/xrow.c
index e7097c3301..9eb6076a0d 100644
--- a/src/box/xrow.c
+++ b/src/box/xrow.c
@@ -1530,30 +1530,26 @@ xrow_decode_auth(const struct xrow_header *row, struct auth_request *request)
 }
 
 void
-xrow_encode_auth(struct xrow_header *packet, const char *salt, size_t salt_len,
+xrow_encode_auth(struct xrow_header *packet,
 		 const char *login, size_t login_len,
-		 const char *password, size_t password_len)
+		 const char *method, size_t method_len,
+		 const char *data, const char *data_end)
 {
 	assert(login != NULL);
+	assert(data != NULL);
 	memset(packet, 0, sizeof(*packet));
-
-	size_t buf_size = XROW_BODY_LEN_MAX + login_len + SCRAMBLE_SIZE;
+	size_t data_size = data_end - data;
+	size_t buf_size = XROW_BODY_LEN_MAX + login_len + data_size;
 	char *buf = xregion_alloc(&fiber()->gc, buf_size);
 	char *d = buf;
-	d = mp_encode_map(d, password != NULL ? 2 : 1);
+	d = mp_encode_map(d, 2);
 	d = mp_encode_uint(d, IPROTO_USER_NAME);
 	d = mp_encode_str(d, login, login_len);
-	if (password != NULL) { /* password can be omitted */
-		assert(salt_len >= SCRAMBLE_SIZE); /* greetingbuf_decode */
-		(void) salt_len;
-		char scramble[SCRAMBLE_SIZE];
-		scramble_prepare(scramble, salt, password, password_len);
-		d = mp_encode_uint(d, IPROTO_TUPLE);
-		d = mp_encode_array(d, 2);
-		d = mp_encode_str(d, "chap-sha1", strlen("chap-sha1"));
-		d = mp_encode_str(d, scramble, SCRAMBLE_SIZE);
-	}
-
+	d = mp_encode_uint(d, IPROTO_TUPLE);
+	d = mp_encode_array(d, 2);
+	d = mp_encode_str(d, method, method_len);
+	memcpy(d, data, data_size);
+	d += data_size;
 	assert(d <= buf + buf_size);
 	packet->body[0].iov_base = buf;
 	packet->body[0].iov_len = (d - buf);
diff --git a/src/box/xrow.h b/src/box/xrow.h
index 7ddc179584..cded47d322 100644
--- a/src/box/xrow.h
+++ b/src/box/xrow.h
@@ -416,17 +416,18 @@ xrow_decode_auth(const struct xrow_header *row, struct auth_request *request);
 /**
  * Encode AUTH command.
  * @param[out] Row.
- * @param salt Salt from IPROTO greeting.
- * @param salt_len Length of @salt.
  * @param login User login.
  * @param login_len Length of @login.
- * @param password User password.
- * @param password_len Length of @password.
-*/
+ * @param method Authentication method.
+ * @param method_len Length of @method.
+ * @param data Authentication request data.
+ * @param data_end End of @data.
+ */
 void
-xrow_encode_auth(struct xrow_header *row, const char *salt, size_t salt_len,
-		 const char *login, size_t login_len, const char *password,
-		 size_t password_len);
+xrow_encode_auth(struct xrow_header *row,
+		 const char *login, size_t login_len,
+		 const char *method, size_t method_len,
+		 const char *data, const char *data_end);
 
 /** Reply to IPROTO_VOTE request. */
 struct ballot {
diff --git a/test/box-luatest/ghs_16_user_enumeration_test.lua b/test/box-luatest/ghs_16_user_enumeration_test.lua
index d66207a3e8..6171ded111 100644
--- a/test/box-luatest/ghs_16_user_enumeration_test.lua
+++ b/test/box-luatest/ghs_16_user_enumeration_test.lua
@@ -95,13 +95,17 @@ g.test_user_enum_on_malformed_auth = function()
             box.error.INVALID_MSGPACK,
             'Invalid MsgPack - authentication request body',
         }, msg)
+        t.assert_equals(auth(uri, user, {'foobar', 'foobar'}), {
+            box.error.UNKNOWN_AUTH_METHOD,
+            "Unknown authentication method 'foobar'",
+        }, msg)
         t.assert_equals(auth(uri, user, {'chap-sha1', 42}), {
-            box.error.INVALID_MSGPACK,
-            'Invalid MsgPack - authentication scramble',
+            box.error.INVALID_AUTH_REQUEST,
+            "Invalid 'chap-sha1' request: scramble must be string",
         }, msg)
         t.assert_equals(auth(uri, user, {'chap-sha1', 'foobar'}), {
-            box.error.INVALID_MSGPACK,
-            'Invalid MsgPack - invalid scramble size',
+            box.error.INVALID_AUTH_REQUEST,
+            "Invalid 'chap-sha1' request: invalid scramble size",
         }, msg)
         t.assert_equals(auth(uri, user, {'chap-sha1', string.rep('x', 20)}), {
             box.error.CREDS_MISMATCH,
diff --git a/test/box/error.result b/test/box/error.result
index d162cfd264..8dff49d04a 100644
--- a/test/box/error.result
+++ b/test/box/error.result
@@ -467,6 +467,9 @@ t;
  |   246: box.error.INTERFERING_ELECTIONS
  |   247: box.error.ITERATOR_POSITION
  |   248: box.error.DDL_NOT_ALLOWED
+ |   249: box.error.UNKNOWN_AUTH_METHOD
+ |   250: box.error.INVALID_AUTH_DATA
+ |   251: box.error.INVALID_AUTH_REQUEST
  | ...
 
 test_run:cmd("setopt delimiter ''");
-- 
GitLab