libpq: Add support for require_auth to control authorized auth methods
authorMichael Paquier <[email protected]>
Tue, 14 Mar 2023 05:00:05 +0000 (14:00 +0900)
committerMichael Paquier <[email protected]>
Tue, 14 Mar 2023 05:00:05 +0000 (14:00 +0900)
The new connection parameter require_auth allows a libpq client to
define a list of comma-separated acceptable authentication types for use
with the server.  There is no negotiation: if the server does not
present one of the allowed authentication requests, the connection
attempt done by the client fails.

The following keywords can be defined in the list:
- password, for AUTH_REQ_PASSWORD.
- md5, for AUTH_REQ_MD5.
- gss, for AUTH_REQ_GSS[_CONT].
- sspi, for AUTH_REQ_SSPI and AUTH_REQ_GSS_CONT.
- scram-sha-256, for AUTH_REQ_SASL[_CONT|_FIN].
- creds, for AUTH_REQ_SCM_CREDS (perhaps this should be removed entirely
now).
- none, to control unauthenticated connections.

All the methods that can be defined in the list can be negated, like
"!password", in which case the server must NOT use the listed
authentication type.  The special method "none" allows/disallows the use
of unauthenticated connections (but it does not govern transport-level
authentication via TLS or GSSAPI).

Internally, the patch logic is tied to check_expected_areq(), that was
used for channel_binding, ensuring that an incoming request is
compatible with conn->require_auth.  It also introduces a new flag,
conn->client_finished_auth, which is set by various authentication
routines when the client side of the handshake is finished.  This
signals to check_expected_areq() that an AUTH_REQ_OK from the server is
expected, and allows the client to complain if the server bypasses
authentication entirely, with for example the reception of a too-early
AUTH_REQ_OK message.

Regression tests are added in authentication TAP tests for all the
keywords supported (except "creds", because it is around only for
compatibility reasons).  A new TAP script has been added for SSPI, as
there was no script dedicated to it yet.  It relies on SSPI being the
default authentication method on Windows, as set by pg_regress.

Author: Jacob Champion
Reviewed-by: Peter Eisentraut, David G. Johnston, Michael Paquier
Discussion: https://postgr.es/m/9e5a8ccddb8355ea9fa4b75a1e3a9edc88a70cd3[email protected]

12 files changed:
doc/src/sgml/libpq.sgml
src/include/libpq/pqcomm.h
src/interfaces/libpq/fe-auth-scram.c
src/interfaces/libpq/fe-auth.c
src/interfaces/libpq/fe-connect.c
src/interfaces/libpq/libpq-int.h
src/test/authentication/meson.build
src/test/authentication/t/001_password.pl
src/test/authentication/t/005_sspi.pl [new file with mode: 0644]
src/test/kerberos/t/001_auth.pl
src/test/ldap/t/001_auth.pl
src/test/ssl/t/002_scram.pl

index 3ccd8ff94216d5f6109bce757257410b5a66076a..3706d349abc61c54c297266b771f7db5e292136b 100644 (file)
@@ -1220,6 +1220,111 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-require-auth" xreflabel="require_auth">
+      <term><literal>require_auth</literal></term>
+      <listitem>
+      <para>
+        Specifies the authentication method that the client requires from the
+        server. If the server does not use the required method to authenticate
+        the client, or if the authentication handshake is not fully completed by
+        the server, the connection will fail. A comma-separated list of methods
+        may also be provided, of which the server must use exactly one in order
+        for the connection to succeed. By default, any authentication method is
+        accepted, and the server is free to skip authentication altogether.
+      </para>
+      <para>
+        Methods may be negated with the addition of a <literal>!</literal>
+        prefix, in which case the server must <emphasis>not</emphasis> attempt
+        the listed method; any other method is accepted, and the server is free
+        not to authenticate the client at all. If a comma-separated list is
+        provided, the server may not attempt <emphasis>any</emphasis> of the
+        listed negated methods. Negated and non-negated forms may not be
+        combined in the same setting.
+      </para>
+      <para>
+        As a final special case, the <literal>none</literal> method requires the
+        server not to use an authentication challenge. (It may also be negated,
+        to require some form of authentication.)
+      </para>
+      <para>
+        The following methods may be specified:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>password</literal></term>
+          <listitem>
+           <para>
+            The server must request plaintext password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>md5</literal></term>
+          <listitem>
+           <para>
+            The server must request MD5 hashed password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>gss</literal></term>
+          <listitem>
+           <para>
+            The server must either request a Kerberos handshake via
+            <acronym>GSSAPI</acronym> or establish a
+            <acronym>GSS</acronym>-encrypted channel (see also
+            <xref linkend="libpq-connect-gssencmode" />).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>sspi</literal></term>
+          <listitem>
+           <para>
+            The server must request Windows <acronym>SSPI</acronym>
+            authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>scram-sha-256</literal></term>
+          <listitem>
+           <para>
+            The server must successfully complete a SCRAM-SHA-256 authentication
+            exchange with the client.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>creds</literal></term>
+          <listitem>
+           <para>
+            The server must request SCM credential authentication (deprecated
+            as of <productname>PostgreSQL</productname> 9.1).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>none</literal></term>
+          <listitem>
+           <para>
+            The server must not prompt the client for an authentication
+            exchange. (This does not prohibit client certificate authentication
+            via TLS, nor GSS authentication via its encrypted transport.)
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
       <term><literal>channel_binding</literal></term>
       <listitem>
@@ -7774,6 +7879,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGREQUIREAUTH</envar></primary>
+      </indexterm>
+      <envar>PGREQUIREAUTH</envar> behaves the same as the <xref
+      linkend="libpq-connect-require-auth"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
index 66ba359390f07637c452e51be2ccd15d786b8426..5268d442abe706d260698518d281019947290107 100644 (file)
@@ -123,6 +123,7 @@ extern PGDLLIMPORT bool Db_user_namespace;
 #define AUTH_REQ_SASL     10   /* Begin SASL authentication */
 #define AUTH_REQ_SASL_CONT 11  /* Continue SASL authentication */
 #define AUTH_REQ_SASL_FIN  12  /* Final SASL message */
+#define AUTH_REQ_MAX      AUTH_REQ_SASL_FIN    /* maximum AUTH_REQ_* value */
 
 typedef uint32 AuthRequest;
 
index 12c3d0bc3332ba737b21e914d59e35fb8b5b5d65..277f72b280c62f03ee66ce71873a91d8a1da6995 100644 (file)
@@ -282,6 +282,7 @@ scram_exchange(void *opaq, char *input, int inputlen,
                        }
                        *done = true;
                        state->state = FE_SCRAM_FINISHED;
+                       state->conn->client_finished_auth = true;
                        break;
 
                default:
index ab454e6cd02136a9dc30847d1799ded0c11e1a0c..a3b80dc550f609b062532e21f4a919dbdacf88db 100644 (file)
@@ -136,7 +136,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
        }
 
        if (maj_stat == GSS_S_COMPLETE)
+       {
+               conn->client_finished_auth = true;
                gss_release_name(&lmin_s, &conn->gtarg_nam);
+       }
 
        return STATUS_OK;
 }
@@ -321,6 +324,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen)
                FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
        }
 
+       if (r == SEC_E_OK)
+               conn->client_finished_auth = true;
+
        /* Cleanup is handled by the code in freePGconn() */
        return STATUS_OK;
 }
@@ -735,6 +741,8 @@ pg_local_sendauth(PGconn *conn)
                                                  strerror_r(errno, sebuf, sizeof(sebuf)));
                return STATUS_ERROR;
        }
+
+       conn->client_finished_auth = true;
        return STATUS_OK;
 #else
        libpq_append_conn_error(conn, "SCM_CRED authentication method not supported");
@@ -805,6 +813,41 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
        return ret;
 }
 
+/*
+ * Translate a disallowed AuthRequest code into an error message.
+ */
+static const char *
+auth_method_description(AuthRequest areq)
+{
+       switch (areq)
+       {
+               case AUTH_REQ_PASSWORD:
+                       return libpq_gettext("server requested a cleartext password");
+               case AUTH_REQ_MD5:
+                       return libpq_gettext("server requested a hashed password");
+               case AUTH_REQ_GSS:
+               case AUTH_REQ_GSS_CONT:
+                       return libpq_gettext("server requested GSSAPI authentication");
+               case AUTH_REQ_SSPI:
+                       return libpq_gettext("server requested SSPI authentication");
+               case AUTH_REQ_SCM_CREDS:
+                       return libpq_gettext("server requested UNIX socket credentials");
+               case AUTH_REQ_SASL:
+               case AUTH_REQ_SASL_CONT:
+               case AUTH_REQ_SASL_FIN:
+                       return libpq_gettext("server requested SASL authentication");
+       }
+
+       return libpq_gettext("server requested an unknown authentication type");
+}
+
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask.  Caller
+ * must ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_method_allowed(conn, type) \
+       (((conn)->allowed_auth_methods & (1 << (type))) != 0)
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -814,6 +857,99 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
        bool            result = true;
+       const char *reason = NULL;
+
+       StaticAssertDecl((sizeof(conn->allowed_auth_methods) * CHAR_BIT) > AUTH_REQ_MAX,
+                                        "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
+
+       /*
+        * If the user required a specific auth method, or specified an allowed
+        * set, then reject all others here, and make sure the server actually
+        * completes an authentication exchange.
+        */
+       if (conn->require_auth)
+       {
+               switch (areq)
+               {
+                       case AUTH_REQ_OK:
+
+                               /*
+                                * Check to make sure we've actually finished our exchange (or
+                                * else that the user has allowed an authentication-less
+                                * connection).
+                                *
+                                * If the user has allowed both SCRAM and unauthenticated
+                                * (trust) connections, then this check will silently accept
+                                * partial SCRAM exchanges, where a misbehaving server does
+                                * not provide its verifier before sending an OK.  This is
+                                * consistent with historical behavior, but it may be a point
+                                * to revisit in the future, since it could allow a server
+                                * that doesn't know the user's password to silently harvest
+                                * material for a brute force attack.
+                                */
+                               if (!conn->auth_required || conn->client_finished_auth)
+                                       break;
+
+                               /*
+                                * No explicit authentication request was made by the server
+                                * -- or perhaps it was made and not completed, in the case of
+                                * SCRAM -- but there is one special case to check.  If the
+                                * user allowed "gss", then a GSS-encrypted channel also
+                                * satisfies the check.
+                                */
+#ifdef ENABLE_GSS
+                               if (auth_method_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+                               {
+                                       /*
+                                        * If implicit GSS auth has already been performed via GSS
+                                        * encryption, we don't need to have performed an
+                                        * AUTH_REQ_GSS exchange.  This allows require_auth=gss to
+                                        * be combined with gssencmode, since there won't be an
+                                        * explicit authentication request in that case.
+                                        */
+                               }
+                               else
+#endif
+                               {
+                                       reason = libpq_gettext("server did not complete authentication");
+                                       result = false;
+                               }
+
+                               break;
+
+                       case AUTH_REQ_PASSWORD:
+                       case AUTH_REQ_MD5:
+                       case AUTH_REQ_GSS:
+                       case AUTH_REQ_GSS_CONT:
+                       case AUTH_REQ_SSPI:
+                       case AUTH_REQ_SCM_CREDS:
+                       case AUTH_REQ_SASL:
+                       case AUTH_REQ_SASL_CONT:
+                       case AUTH_REQ_SASL_FIN:
+
+                               /*
+                                * We don't handle these with the default case, to avoid
+                                * bit-shifting past the end of the allowed_auth_methods mask
+                                * if the server sends an unexpected AuthRequest.
+                                */
+                               result = auth_method_allowed(conn, areq);
+                               break;
+
+                       default:
+                               result = false;
+                               break;
+               }
+       }
+
+       if (!result)
+       {
+               if (!reason)
+                       reason = auth_method_description(areq);
+
+               libpq_append_conn_error(conn, "auth method \"%s\" requirement failed: %s",
+                                                               conn->require_auth, reason);
+               return result;
+       }
 
        /*
         * When channel_binding=require, we must protect against two cases: (1) we
@@ -1008,6 +1144,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
                                                                                 "fe_sendauth: error sending password authentication\n");
                                        return STATUS_ERROR;
                                }
+
+                               /* We expect no further authentication requests. */
+                               conn->client_finished_auth = true;
                                break;
                        }
 
index 5638b223cb4521555fd3434f67ebaeee9af4f3ee..dd4b98e09985dc3680365c6d64f07809b7d2043a 100644 (file)
@@ -307,6 +307,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
                "Require-Peer", "", 10,
        offsetof(struct pg_conn, requirepeer)},
 
+       {"require_auth", "PGREQUIREAUTH", NULL, NULL,
+               "Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+       offsetof(struct pg_conn, require_auth)},
+
        {"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
                "SSL-Minimum-Protocol-Version", "", 8,  /* sizeof("TLSv1.x") == 8 */
        offsetof(struct pg_conn, ssl_min_protocol_version)},
@@ -595,6 +599,7 @@ pqDropServerData(PGconn *conn)
        /* Reset assorted other per-connection state */
        conn->last_sqlstate[0] = '\0';
        conn->auth_req_received = false;
+       conn->client_finished_auth = false;
        conn->password_needed = false;
        conn->write_failed = false;
        free(conn->write_err_msg);
@@ -1237,6 +1242,170 @@ connectOptions2(PGconn *conn)
                }
        }
 
+       /*
+        * parse and validate require_auth option
+        */
+       if (conn->require_auth && conn->require_auth[0])
+       {
+               char       *s = conn->require_auth;
+               bool            first,
+                                       more;
+               bool            negated = false;
+
+               /*
+                * By default, start from an empty set of allowed options and add to
+                * it.
+                */
+               conn->auth_required = true;
+               conn->allowed_auth_methods = 0;
+
+               for (first = true, more = true; more; first = false)
+               {
+                       char       *method,
+                                          *part;
+                       uint32          bits;
+
+                       part = parse_comma_separated_list(&s, &more);
+                       if (part == NULL)
+                               goto oom_error;
+
+                       /*
+                        * Check for negation, e.g. '!password'. If one element is
+                        * negated, they all have to be.
+                        */
+                       method = part;
+                       if (*method == '!')
+                       {
+                               if (first)
+                               {
+                                       /*
+                                        * Switch to a permissive set of allowed options, and
+                                        * subtract from it.
+                                        */
+                                       conn->auth_required = false;
+                                       conn->allowed_auth_methods = -1;
+                               }
+                               else if (!negated)
+                               {
+                                       conn->status = CONNECTION_BAD;
+                                       libpq_append_conn_error(conn, "negative require_auth method \"%s\" cannot be mixed with non-negative methods",
+                                                                                       method);
+
+                                       free(part);
+                                       return false;
+                               }
+
+                               negated = true;
+                               method++;
+                       }
+                       else if (negated)
+                       {
+                               conn->status = CONNECTION_BAD;
+                               libpq_append_conn_error(conn, "require_auth method \"%s\" cannot be mixed with negative methods",
+                                                                               method);
+
+                               free(part);
+                               return false;
+                       }
+
+                       if (strcmp(method, "password") == 0)
+                       {
+                               bits = (1 << AUTH_REQ_PASSWORD);
+                       }
+                       else if (strcmp(method, "md5") == 0)
+                       {
+                               bits = (1 << AUTH_REQ_MD5);
+                       }
+                       else if (strcmp(method, "gss") == 0)
+                       {
+                               bits = (1 << AUTH_REQ_GSS);
+                               bits |= (1 << AUTH_REQ_GSS_CONT);
+                       }
+                       else if (strcmp(method, "sspi") == 0)
+                       {
+                               bits = (1 << AUTH_REQ_SSPI);
+                               bits |= (1 << AUTH_REQ_GSS_CONT);
+                       }
+                       else if (strcmp(method, "scram-sha-256") == 0)
+                       {
+                               /* This currently assumes that SCRAM is the only SASL method. */
+                               bits = (1 << AUTH_REQ_SASL);
+                               bits |= (1 << AUTH_REQ_SASL_CONT);
+                               bits |= (1 << AUTH_REQ_SASL_FIN);
+                       }
+                       else if (strcmp(method, "creds") == 0)
+                       {
+                               bits = (1 << AUTH_REQ_SCM_CREDS);
+                       }
+                       else if (strcmp(method, "none") == 0)
+                       {
+                               /*
+                                * Special case: let the user explicitly allow (or disallow)
+                                * connections where the server does not send an explicit
+                                * authentication challenge, such as "trust" and "cert" auth.
+                                */
+                               if (negated)    /* "!none" */
+                               {
+                                       if (conn->auth_required)
+                                               goto duplicate;
+
+                                       conn->auth_required = true;
+                               }
+                               else                    /* "none" */
+                               {
+                                       if (!conn->auth_required)
+                                               goto duplicate;
+
+                                       conn->auth_required = false;
+                               }
+
+                               free(part);
+                               continue;               /* avoid the bitmask manipulation below */
+                       }
+                       else
+                       {
+                               conn->status = CONNECTION_BAD;
+                               libpq_append_conn_error(conn, "invalid require_auth method: \"%s\"",
+                                                                               method);
+
+                               free(part);
+                               return false;
+                       }
+
+                       /* Update the bitmask. */
+                       if (negated)
+                       {
+                               if ((conn->allowed_auth_methods & bits) == 0)
+                                       goto duplicate;
+
+                               conn->allowed_auth_methods &= ~bits;
+                       }
+                       else
+                       {
+                               if ((conn->allowed_auth_methods & bits) == bits)
+                                       goto duplicate;
+
+                               conn->allowed_auth_methods |= bits;
+                       }
+
+                       free(part);
+                       continue;
+
+       duplicate:
+
+                       /*
+                        * A duplicated method probably indicates a typo in a setting
+                        * where typos are extremely risky.
+                        */
+                       conn->status = CONNECTION_BAD;
+                       libpq_append_conn_error(conn, "require_auth method \"%s\" is specified more than once",
+                                                                       part);
+
+                       free(part);
+                       return false;
+               }
+       }
+
        /*
         * validate channel_binding option
         */
@@ -4055,6 +4224,7 @@ freePGconn(PGconn *conn)
        free(conn->sslcompression);
        free(conn->sslsni);
        free(conn->requirepeer);
+       free(conn->require_auth);
        free(conn->ssl_min_protocol_version);
        free(conn->ssl_max_protocol_version);
        free(conn->gssencmode);
index d7ec5ed4293e3f366537a197e8fbb628e2206d06..1dc264fe544962e9cad44a840dfcffccac67a6ee 100644 (file)
@@ -396,6 +396,7 @@ struct pg_conn
        char       *ssl_min_protocol_version;   /* minimum TLS protocol version */
        char       *ssl_max_protocol_version;   /* maximum TLS protocol version */
        char       *target_session_attrs;       /* desired session properties */
+       char       *require_auth;       /* name of the expected auth method */
 
        /* Optional file to write trace info to */
        FILE       *Pfdebug;
@@ -457,6 +458,14 @@ struct pg_conn
        bool            write_failed;   /* have we had a write failure on sock? */
        char       *write_err_msg;      /* write error message, or NULL if OOM */
 
+       bool            auth_required;  /* require an authentication challenge from
+                                                                * the server? */
+       uint32          allowed_auth_methods;   /* bitmask of acceptable AuthRequest
+                                                                                * codes */
+       bool            client_finished_auth;   /* have we finished our half of the
+                                                                                * authentication exchange? */
+
+
        /* Transient state needed while establishing connection */
        PGTargetServerType target_server_type;  /* desired session properties */
        bool            try_next_addr;  /* time to advance to next address/host? */
index 3fe279fc108cf9bc95a4463df06186f7bccbad18..e50104178586d17b274507e3c27a27d31d3bd34d 100644 (file)
@@ -10,6 +10,7 @@ tests += {
       't/002_saslprep.pl',
       't/003_peer.pl',
       't/004_file_inclusion.pl',
+      't/005_sspi.pl',
     ],
   },
 }
index a2fde1408bb05f844cd77a942bf153920757de80..cba5d7d6487070ba9572bbb9247c2e7157f9efd7 100644 (file)
@@ -115,6 +115,114 @@ is($res, 't',
        "users with trust authentication use SYSTEM_USER = NULL in parallel workers"
 );
 
+# Explicitly specifying an empty require_auth (the default) should always
+# succeed.
+$node->connect_ok("user=scram_role require_auth=",
+       "empty require_auth succeeds");
+
+# All these values of require_auth should fail, as trust is expected.
+$node->connect_fails(
+       "user=scram_role require_auth=gss",
+       "GSS authentication required, fails with trust auth",
+       expected_stderr =>
+         qr/auth method "gss" requirement failed: server did not complete authentication/
+);
+$node->connect_fails(
+       "user=scram_role require_auth=sspi",
+       "SSPI authentication required, fails with trust auth",
+       expected_stderr =>
+         qr/auth method "sspi" requirement failed: server did not complete authentication/
+);
+$node->connect_fails(
+       "user=scram_role require_auth=password",
+       "password authentication required, fails with trust auth",
+       expected_stderr =>
+         qr/auth method "password" requirement failed: server did not complete authentication/
+);
+$node->connect_fails(
+       "user=scram_role require_auth=md5",
+       "MD5 authentication required, fails with trust auth",
+       expected_stderr =>
+         qr/auth method "md5" requirement failed: server did not complete authentication/
+);
+$node->connect_fails(
+       "user=scram_role require_auth=scram-sha-256",
+       "SCRAM authentication required, fails with trust auth",
+       expected_stderr =>
+         qr/auth method "scram-sha-256" requirement failed: server did not complete authentication/
+);
+$node->connect_fails(
+       "user=scram_role require_auth=password,scram-sha-256",
+       "password and SCRAM authentication required, fails with trust auth",
+       expected_stderr =>
+         qr/auth method "password,scram-sha-256" requirement failed: server did not complete authentication/
+);
+
+# These negative patterns of require_auth should succeed.
+$node->connect_ok("user=scram_role require_auth=!gss",
+       "GSS authentication can be forbidden, succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!sspi",
+       "SSPI authentication can be forbidden, succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password",
+       "password authentication can be forbidden, succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!md5",
+       "md5 authentication can be forbidden, succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!scram-sha-256",
+       "SCRAM authentication can be forbidden, succeeds with trust auth");
+$node->connect_ok(
+       "user=scram_role require_auth=!password,!scram-sha-256",
+       "multiple authentication types forbidden, succeeds with trust auth");
+
+# require_auth=[!]none should interact correctly with trust auth.
+$node->connect_ok("user=scram_role require_auth=none",
+       "all authentication types forbidden, succeeds with trust auth");
+$node->connect_fails(
+       "user=scram_role require_auth=!none",
+       "any authentication types required, fails with trust auth",
+       expected_stderr => qr/server did not complete authentication/);
+
+# Negative and positive require_auth methods can't be mixed.
+$node->connect_fails(
+       "user=scram_role require_auth=scram-sha-256,!md5",
+       "negative require_auth methods cannot be mixed with positive ones",
+       expected_stderr =>
+         qr/negative require_auth method "!md5" cannot be mixed with non-negative methods/
+);
+$node->connect_fails(
+       "user=scram_role require_auth=!password,!none,scram-sha-256",
+       "positive require_auth methods cannot be mixed with negative one",
+       expected_stderr =>
+         qr/require_auth method "scram-sha-256" cannot be mixed with negative methods/
+);
+
+# require_auth methods cannot have duplicated values.
+$node->connect_fails(
+       "user=scram_role require_auth=password,md5,password",
+       "require_auth methods cannot include duplicates, positive case",
+       expected_stderr =>
+         qr/require_auth method "password" is specified more than once/);
+$node->connect_fails(
+       "user=scram_role require_auth=!password,!md5,!password",
+       "require_auth methods cannot be duplicated, negative case",
+       expected_stderr =>
+         qr/require_auth method "!password" is specified more than once/);
+$node->connect_fails(
+       "user=scram_role require_auth=none,md5,none",
+       "require_auth methods cannot be duplicated, none case",
+       expected_stderr =>
+         qr/require_auth method "none" is specified more than once/);
+$node->connect_fails(
+       "user=scram_role require_auth=!none,!md5,!none",
+       "require_auth methods cannot be duplicated, !none case",
+       expected_stderr =>
+         qr/require_auth method "!none" is specified more than once/);
+
+# Unknown value defined in require_auth.
+$node->connect_fails(
+       "user=scram_role require_auth=none,abcdefg",
+       "unknown require_auth methods are rejected",
+       expected_stderr => qr/invalid require_auth method: "abcdefg"/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'all', 'all', 'password');
 test_conn($node, 'user=scram_role', 'password', 0,
@@ -124,6 +232,47 @@ test_conn($node, 'user=md5_role', 'password', 0,
        log_like =>
          [qr/connection authenticated: identity="md5_role" method=password/]);
 
+# require_auth succeeds here with a plaintext password.
+$node->connect_ok("user=scram_role require_auth=password",
+       "password authentication required, works with password auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+       "any authentication required, works with password auth");
+$node->connect_ok(
+       "user=scram_role require_auth=scram-sha-256,password,md5",
+       "multiple authentication types required, works with password auth");
+
+# require_auth fails for other authentication types.
+$node->connect_fails(
+       "user=scram_role require_auth=md5",
+       "md5 authentication required, fails with password auth",
+       expected_stderr =>
+         qr/auth method "md5" requirement failed: server requested a cleartext password/
+);
+$node->connect_fails(
+       "user=scram_role require_auth=scram-sha-256",
+       "SCRAM authentication required, fails with password auth",
+       expected_stderr =>
+         qr/auth method "scram-sha-256" requirement failed: server requested a cleartext password/
+);
+$node->connect_fails(
+       "user=scram_role require_auth=none",
+       "all authentication forbidden, fails with password auth",
+       expected_stderr =>
+         qr/auth method "none" requirement failed: server requested a cleartext password/
+);
+
+# Disallowing password authentication fails, even if requested by server.
+$node->connect_fails(
+       "user=scram_role require_auth=!password",
+       "password authentication forbidden, fails with password auth",
+       expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails(
+       "user=scram_role require_auth=!password,!md5,!scram-sha-256",
+       "multiple authentication types forbidden, fails with password auth",
+       expected_stderr =>
+         qr/ method "!password,!md5,!scram-sha-256" requirement failed: server requested a cleartext password/
+);
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'all', 'all', 'scram-sha-256');
 test_conn(
@@ -137,6 +286,46 @@ test_conn(
 test_conn($node, 'user=md5_role', 'scram-sha-256', 2,
        log_unlike => [qr/connection authenticated:/]);
 
+# require_auth should succeeds with SCRAM when it is required.
+$node->connect_ok(
+       "user=scram_role require_auth=scram-sha-256",
+       "SCRAM authentication required, works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+       "any authentication required, works with SCRAM auth");
+$node->connect_ok(
+       "user=scram_role require_auth=password,scram-sha-256,md5",
+       "multiple authentication types required, works with SCRAM auth");
+
+# Authentication fails for other authentication types.
+$node->connect_fails(
+       "user=scram_role require_auth=password",
+       "password authentication required, fails with SCRAM auth",
+       expected_stderr =>
+         qr/auth method "password" requirement failed: server requested SASL authentication/
+);
+$node->connect_fails(
+       "user=scram_role require_auth=md5",
+       "md5 authentication required, fails with SCRAM auth",
+       expected_stderr =>
+         qr/auth method "md5" requirement failed: server requested SASL authentication/
+);
+$node->connect_fails(
+       "user=scram_role require_auth=none",
+       "all authentication forbidden, fails with SCRAM auth",
+       expected_stderr =>
+         qr/auth method "none" requirement failed: server requested SASL authentication/
+);
+
+# Authentication fails if SCRAM authentication is forbidden.
+$node->connect_fails(
+       "user=scram_role require_auth=!scram-sha-256",
+       "SCRAM authentication forbidden, fails with SCRAM auth",
+       expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails(
+       "user=scram_role require_auth=!password,!md5,!scram-sha-256",
+       "multiple authentication types forbidden, fails with SCRAM auth",
+       expected_stderr => qr/server requested SASL authentication/);
+
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_conn($node, 'user=scram_role', 'scram-sha-256', 2,
@@ -153,6 +342,49 @@ test_conn($node, 'user=md5_role', 'md5', 0,
        log_like =>
          [qr/connection authenticated: identity="md5_role" method=md5/]);
 
+# require_auth succeeds with MD5 required.
+$node->connect_ok("user=md5_role require_auth=md5",
+       "MD5 authentication required, works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=!none",
+       "any authentication required, works with MD5 auth");
+$node->connect_ok(
+       "user=md5_role require_auth=md5,scram-sha-256,password",
+       "multiple authentication types required, works with MD5 auth");
+
+# Authentication fails if other types are required.
+$node->connect_fails(
+       "user=md5_role require_auth=password",
+       "password authentication required, fails with MD5 auth",
+       expected_stderr =>
+         qr/auth method "password" requirement failed: server requested a hashed password/
+);
+$node->connect_fails(
+       "user=md5_role require_auth=scram-sha-256",
+       "SCRAM authentication required, fails with MD5 auth",
+       expected_stderr =>
+         qr/auth method "scram-sha-256" requirement failed: server requested a hashed password/
+);
+$node->connect_fails(
+       "user=md5_role require_auth=none",
+       "all authentication types forbidden, fails with MD5 auth",
+       expected_stderr =>
+         qr/auth method "none" requirement failed: server requested a hashed password/
+);
+
+# Authentication fails if MD5 is forbidden.
+$node->connect_fails(
+       "user=md5_role require_auth=!md5",
+       "password authentication forbidden, fails with MD5 auth",
+       expected_stderr =>
+         qr/auth method "!md5" requirement failed: server requested a hashed password/
+);
+$node->connect_fails(
+       "user=md5_role require_auth=!password,!md5,!scram-sha-256",
+       "multiple authentication types forbidden, fails with MD5 auth",
+       expected_stderr =>
+         qr/auth method "!password,!md5,!scram-sha-256" requirement failed: server requested a hashed password/
+);
+
 # Test SYSTEM_USER <> NULL with parallel workers.
 $node->safe_psql(
        'postgres',
diff --git a/src/test/authentication/t/005_sspi.pl b/src/test/authentication/t/005_sspi.pl
new file mode 100644 (file)
index 0000000..eed0fb1
--- /dev/null
@@ -0,0 +1,41 @@
+
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Tests targeting SSPI on Windows.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+if (!$windows_os || $use_unix_sockets)
+{
+       plan skip_all =>
+         "SSPI tests require Windows (without PG_TEST_USE_UNIX_SOCKETS)";
+}
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
+$node->start;
+
+# SSPI is set up by default.  Make sure it interacts correctly with
+# require_auth.
+$node->connect_ok("require_auth=sspi",
+       "SSPI authentication required, works with SSPI auth");
+$node->connect_fails(
+       "require_auth=!sspi",
+       "SSPI authentication forbidden, fails with SSPI auth",
+       expected_stderr =>
+         qr/auth method "!sspi" requirement failed: server requested SSPI authentication/
+);
+$node->connect_fails(
+       "require_auth=scram-sha-256",
+       "SCRAM authentication required, fails with SSPI auth",
+       expected_stderr =>
+         qr/auth method "scram-sha-256" requirement failed: server requested SSPI authentication/
+);
+
+done_testing();
index 3bc4ad7dd36d23eefff2ff84364f5ec761e42695..a0ed3a0a0b5d6dab1e99e78ce975966f0efd7df4 100644 (file)
@@ -337,6 +337,32 @@ test_query(
        'gssencmode=require',
        'sending 100K lines works');
 
+# require_auth=gss succeeds if required.
+$node->connect_ok(
+       $node->connstr('postgres')
+         . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss",
+       "GSS authentication requested, works with non-encyrpted GSS");
+$node->connect_ok(
+       $node->connstr('postgres')
+         . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+       "GSS authentication requested, works with encrypted GSS auth");
+
+# require_auth=sspi fails if required.
+$node->connect_fails(
+       $node->connstr('postgres')
+         . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi",
+       "SSPI authentication requested, fails with non-encrypted GSS",
+       expected_stderr =>
+         qr/auth method "sspi" requirement failed: server requested GSSAPI authentication/
+);
+$node->connect_fails(
+       $node->connstr('postgres')
+         . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi",
+       "SSPI authentication requested, fails with encrypted GSS",
+       expected_stderr =>
+         qr/auth method "sspi" requirement failed: server did not complete authentication/
+);
+
 # Test that SYSTEM_USER works.
 test_query($node, 'test1', 'SELECT SYSTEM_USER;',
        qr/^gss:test1\@$realm$/s, 'gssencmode=require', 'testing system_user');
@@ -382,6 +408,16 @@ test_access(
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
        'fails with GSS encryption disabled and hostgssenc hba');
 
+# require_auth=gss succeeds if required.
+$node->connect_ok(
+       $node->connstr('postgres')
+         . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+       "GSS authentication requested, works with GSS encryption");
+$node->connect_ok(
+       $node->connstr('postgres')
+         . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256",
+       "multiple authentication types requested, works with GSS encryption");
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
        qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
index f3ed806ec23f82f64dfcd848b6e4fd4b6d6d48d2..1e027ced0117b88c30860d91a399887023fc3bc0 100644 (file)
@@ -101,6 +101,12 @@ test_access(
                qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
        ],);
 
+# require_auth=password should complete successfully; other methods should fail.
+$node->connect_ok("user=test1 require_auth=password",
+       "password authentication required, works with ldap auth");
+$node->connect_fails("user=test1 require_auth=scram-sha-256",
+       "SCRAM authentication required, fails with ldap auth");
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
index 1d3905d3a1eb67d1f5ca42bf721171b697f77809..8038135697fb3c29cc2328706e5bd0f6039d7a6b 100644 (file)
@@ -140,6 +140,34 @@ $node->connect_ok(
                qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
        ]);
 
+# channel_binding should continue to work independently of require_auth.
+$node->connect_ok(
+       "$common_connstr user=ssltestuser channel_binding=disable require_auth=scram-sha-256",
+       "SCRAM with SSL, channel_binding=disable, and require_auth=scram-sha-256"
+);
+$node->connect_fails(
+       "$common_connstr user=md5testuser require_auth=md5 channel_binding=require",
+       "channel_binding can fail even when require_auth succeeds",
+       expected_stderr =>
+         qr/channel binding required but not supported by server's authentication request/
+);
+if ($supports_tls_server_end_point)
+{
+       $node->connect_ok(
+               "$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+               "SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256"
+       );
+}
+else
+{
+       $node->connect_fails(
+               "$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+               "SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256",
+               expected_stderr =>
+                 qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
+       );
+}
+
 # Now test with a server certificate that uses the RSA-PSS algorithm.
 # This checks that the certificate can be loaded and that channel binding
 # works. (see bug #17760)