mirror of
https://github.com/postgres/postgres.git
synced 2025-07-02 09:02:37 +03:00
libpq: Add support for require_auth to control authorized auth methods
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
This commit is contained in:
@ -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:
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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? */
|
||||
|
Reference in New Issue
Block a user