From 84ea837dda62b29fcd1cb709802c754f517208ee Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 20 Feb 2017 14:15:18 +0900
Subject: [PATCH 9/9] Make hba configuration for SASL more extensible

The entries of pg_hba.conf are changed from "scram" to that:
"sasl protocol=scram_sha_256". SASL supporting many families of
authentication mechanisms, this likely makes the most sense. protocol
is as well a mandatory parameter. In the future if other mechanisms
are added this will prove to be very handy, by being able for example
to have a list of mechanisms defined.

Documentation and regression tests are updated according to that.
---
 doc/src/sgml/client-auth.sgml             | 46 +++++++++++++++++++++++--------
 src/backend/libpq/auth.c                  | 10 +++++--
 src/backend/libpq/hba.c                   | 31 +++++++++++++++++++--
 src/backend/libpq/pg_hba.conf.sample      |  4 +--
 src/bin/initdb/initdb.c                   | 14 +++++-----
 src/include/libpq/hba.h                   |  1 +
 src/test/recovery/t/009_authentication.pl | 14 +++++++---
 7 files changed, 91 insertions(+), 29 deletions(-)

diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index f27d417f65..125a07325a 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -423,11 +423,12 @@ hostnossl  <replaceable>database</replaceable>  <replaceable>user</replaceable>
        </varlistentry>
 
        <varlistentry>
-        <term><literal>scram</></term>
+        <term><literal>sasl</></term>
         <listitem>
          <para>
           Require the client to supply a password encrypted with
-          SCRAM-SHA-256 for authentication.
+          a protocol compatible with SASL exchange protocol, like
+          SCRAM-SHA-256, for authentication.
           See <xref linkend="auth-password"> for details.
          </para>
         </listitem>
@@ -684,19 +685,19 @@ host    postgres        all             192.168.93.0/24         ident
 # "postgres" if the user's password is correctly supplied.
 #
 # TYPE  DATABASE        USER            ADDRESS                 METHOD
-host    postgres        all             192.168.12.10/32        scram
+host    postgres        all             192.168.12.10/32        sasl protocol=scram_sha_256
 
 # Allow any user from hosts in the example.com domain to connect to
 # any database if the user's password is correctly supplied.
 #
-# Most users use SCRAM authentication, but some users use older clients
-# that don't support SCRAM authentication, and need to be able to log
-# in using MD5 authentication. Such users are put in the @md5users
-# group, everyone else must use SCRAM.
+# Most users use SCRAM-SHA-256 authentication, but some users use older
+# clients that don't support SCRAM-SHA-256 authentication, and need to be
+# able to log in using MD5 authentication. Such users are put in the @md5users
+# group, everyone else must use SCRAM-SHA-256.
 #
 # TYPE  DATABASE        USER            ADDRESS                 METHOD
 host    all             @md5users       .example.com            md5
-host    all             all             .example.com            scram
+host    all             all             .example.com            sasl protocol=scram_sha_256
 
 # In the absence of preceding "host" lines, these two lines will
 # reject all connections from 192.168.54.1 (since that entry will be
@@ -942,8 +943,8 @@ omicron         bryanh                  guest1
    </para>
 
    <para>
-    <literal>scram</>, as described in
-    <ulink url="https://tools.ietf.org/html/rfc5802">RFC5802</ulink> is
+    <literal>sasl</>, as described in
+    <ulink url="https://tools.ietf.org/html/rfc4422">RFC4422</ulink> is
     defined to be more robust more than <literal>md5</> from a security
     point of view as it protects from cases where the hashed password is
     taken directly from <structname>pg_authid</structname> in which case
@@ -951,8 +952,29 @@ omicron         bryanh                  guest1
     the password behind it.  It protects as well from password interception
     and data sniffing where the password data could be directly obtained
     from the network as well as man-in-the-middle (MITM) attacks.  So it
-    is strongly encouraged to use <literal>scram</> over <literal>md5</> for
-    password-based deployments.
+    is strongly encouraged to use <literal>sasl</> over <literal>md5</> for
+    password-based deployments. <productname>SASL</> supports many families
+    of authentication mechanisms, <productname>PostgreSQL</> supporting only
+    <literal>SCRAM-SHA-256</literal> as described in
+    <ulink url="https://tools.ietf.org/html/rfc5802">RFC5802</ulink>.
+   </para>
+
+   <para>
+    The following configuration options are supported for <productname>SASL</productname>:
+    <variablelist>
+     <varlistentry>
+      <term><literal>protocol</literal></term>
+      <listitem>
+       <para>
+        Sets the exchange protocol supported. This parameter is mandatory
+        and only the authentication mechanism listed are supported for the
+        user attempting a connection. It can be to <literal>scram_sha_256</>
+        to authorize users to connect using <literal>SCRAM-SHA-256</literal>
+        as authentication mechanism for the SASL exchange protocol.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
    </para>
 
    <para>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index e475366ef3..2adfb8c4d4 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -818,8 +818,14 @@ CheckSASLAuth(Port *port, char **logdetail)
 	 * guess that a server is expecting SASL or MD5 depending on the answer
 	 * given by the backend without the user providing a password first.
 	 */
-	sendAuthRequest(port, AUTH_REQ_SASL, SCRAM_SHA256_NAME,
-					strlen(SCRAM_SHA256_NAME) + 1);
+	if (strcmp(port->hba->sasl_protocol, "scram_sha_256") == 0)
+		sendAuthRequest(port, AUTH_REQ_SASL, SCRAM_SHA256_NAME,
+						strlen(SCRAM_SHA256_NAME) + 1);
+	else
+		ereport(FATAL,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("SASL protocol %s is not supported",
+						port->hba->sasl_protocol)));
 
 	/*
 	 * If the user doesn't exist, or doesn't have a valid password, or
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 8c793e6389..e8e9a67b64 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -128,7 +128,7 @@ static const char *const UserAuthName[] =
 	"ident",
 	"password",
 	"md5",
-	"scram",
+	"sasl",
 	"gss",
 	"sspi",
 	"pam",
@@ -1327,7 +1327,7 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
 		}
 		parsedline->auth_method = uaMD5;
 	}
-	else if (strcmp(token->string, "scram") == 0)
+	else if (strcmp(token->string, "sasl") == 0)
 		parsedline->auth_method = uaSASL;
 	else if (strcmp(token->string, "pam") == 0)
 #ifdef USE_PAM
@@ -1539,6 +1539,11 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
 		MANDATORY_AUTH_ARG(parsedline->radiussecret, "radiussecret", "radius");
 	}
 
+	if (parsedline->auth_method == uaSASL)
+	{
+		MANDATORY_AUTH_ARG(parsedline->sasl_protocol, "protocol", "sasl");
+	}
+
 	/*
 	 * Enforce any parameters implied by other settings.
 	 */
@@ -1824,6 +1829,21 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 		REQUIRE_AUTH_OPTION(uaRADIUS, "radiusidentifier", "radius");
 		hbaline->radiusidentifier = pstrdup(val);
 	}
+	else if (strcmp(name, "protocol") == 0)
+	{
+		if (hbaline->auth_method != uaSASL)
+			INVALID_AUTH_OPTION("protocol", gettext_noop("sasl"));
+		if (strcmp(val, "scram_sha_256") != 0)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("invalid protocol name for SASL: \"%s\"", val),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			*err_msg = psprintf("invalid protocl name for SASL: \"%s\"", val);
+		}
+		hbaline->sasl_protocol = pstrdup(val);
+	}
 	else
 	{
 		ereport(elevel,
@@ -2080,6 +2100,13 @@ gethba_options(HbaLine *hba)
 		options[noptions++] =
 			CStringGetTextDatum(psprintf("pamservice=%s", hba->pamservice));
 
+	if (hba->auth_method == uaSASL)
+	{
+		if (hba->sasl_protocol)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("protocol=%s", hba->sasl_protocol));
+	}
+
 	if (hba->auth_method == uaLDAP)
 	{
 		if (hba->ldapserver)
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 73f7973ea2..3b04d2367f 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -42,9 +42,9 @@
 # or "samenet" to match any address in any subnet that the server is
 # directly connected to.
 #
-# METHOD can be "trust", "reject", "md5", "password", "scram", "gss",
+# METHOD can be "trust", "reject", "md5", "password", "sasl", "gss",
 # "sspi", "ident", "peer", "pam", "ldap", "radius" or "cert".  Note that
-# "password" sends passwords in clear text; "md5" or "scram" are preferred
+# "password" sends passwords in clear text; "md5" or "sasl" are preferred
 # since they send encrypted passwords.
 #
 # OPTIONS are a set of options for the authentication in the format
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 20ec2150bc..dfa95d05aa 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -76,7 +76,7 @@
 extern const char *select_default_timezone(const char *share_path);
 
 static const char *const auth_methods_host[] = {
-	"trust", "reject", "md5", "password", "scram", "ident", "radius",
+	"trust", "reject", "md5", "password", "sasl", "ident", "radius",
 #ifdef ENABLE_GSS
 	"gss",
 #endif
@@ -98,7 +98,7 @@ static const char *const auth_methods_host[] = {
 	NULL
 };
 static const char *const auth_methods_local[] = {
-	"trust", "reject", "md5", "scram", "password", "peer", "radius",
+	"trust", "reject", "md5", "sasl", "password", "peer", "radius",
 #ifdef USE_PAM
 	"pam", "pam ",
 #endif
@@ -1129,8 +1129,8 @@ setup_config(void)
 							  "#update_process_title = off");
 #endif
 
-	if (strcmp(authmethodlocal, "scram") == 0 ||
-		strcmp(authmethodhost, "scram") == 0)
+	if (strcmp(authmethodlocal, "sasl") == 0 ||
+		strcmp(authmethodhost, "sasl") == 0)
 	{
 		conflines = replace_token(conflines,
 								  "#password_encryption = md5",
@@ -2317,16 +2317,16 @@ check_need_password(const char *authmethodlocal, const char *authmethodhost)
 {
 	if ((strcmp(authmethodlocal, "md5") == 0 ||
 		 strcmp(authmethodlocal, "password") == 0 ||
-		 strcmp(authmethodlocal, "scram") == 0) &&
+		 strcmp(authmethodlocal, "sasl") == 0) &&
 		(strcmp(authmethodhost, "md5") == 0 ||
 		 strcmp(authmethodhost, "password") == 0 ||
-		 strcmp(authmethodlocal, "scram") == 0) &&
+		 strcmp(authmethodlocal, "sasl") == 0) &&
 		!(pwprompt || pwfilename))
 	{
 		fprintf(stderr, _("%s: must specify a password for the superuser to enable %s authentication\n"), progname,
 				(strcmp(authmethodlocal, "md5") == 0 ||
 				 strcmp(authmethodlocal, "password") == 0 ||
-				 strcmp(authmethodlocal, "scram") == 0)
+				 strcmp(authmethodlocal, "sasl") == 0)
 				? authmethodlocal
 				: authmethodhost);
 		exit(1);
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8f55edb16a..51968c98b8 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -93,6 +93,7 @@ typedef struct HbaLine
 	char	   *radiussecret;
 	char	   *radiusidentifier;
 	int			radiusport;
+	char	   *sasl_protocol;
 } HbaLine;
 
 typedef struct IdentLine
diff --git a/src/test/recovery/t/009_authentication.pl b/src/test/recovery/t/009_authentication.pl
index 16be7f5cc8..c68a1093f6 100644
--- a/src/test/recovery/t/009_authentication.pl
+++ b/src/test/recovery/t/009_authentication.pl
@@ -16,11 +16,17 @@ use Test::More tests => 12;
 # and then execute a reload to refresh it.
 sub reset_pg_hba
 {
-	my $node = shift;
-	my $hba_method = shift;
+	my ($node, $hba_method, $options) = @_;
 
 	unlink($node->data_dir . '/pg_hba.conf');
-	$node->append_conf('pg_hba.conf', "local all all $hba_method");
+	if (defined($options))
+	{
+		$node->append_conf('pg_hba.conf', "local all all $hba_method $options");
+	}
+	else
+	{
+		$node->append_conf('pg_hba.conf', "local all all $hba_method");
+	}
 	$node->reload;
 }
 
@@ -71,7 +77,7 @@ SKIP:
 
 	# For "scram" method, user "plain_role" and "scram_role" should be able to
 	# connect.
-	reset_pg_hba($node, 'scram');
+	reset_pg_hba($node, 'sasl', 'protocol=scram_sha_256');
 	test_role($node, 'scram_role', 'scram', 0);
 	test_role($node, 'md5_role', 'scram', 2);
 	test_role($node, 'plain_role', 'scram', 0);
-- 
2.11.1

