Skip to content

Commit

Permalink
libpq: Add support for require_auth to control authorized auth methods
Browse files Browse the repository at this point in the history
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.camel@vmware.com
  • Loading branch information
michaelpq committed Mar 14, 2023
1 parent 7274009 commit 3a465cc
Show file tree
Hide file tree
Showing 12 changed files with 779 additions and 0 deletions.
115 changes: 115 additions & 0 deletions doc/src/sgml/libpq.sgml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
1 change: 1 addition & 0 deletions src/include/libpq/pqcomm.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
1 change: 1 addition & 0 deletions src/interfaces/libpq/fe-auth-scram.c
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
139 changes: 139 additions & 0 deletions src/interfaces/libpq/fe-auth.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down
Loading

0 comments on commit 3a465cc

Please sign in to comment.