sasl: Allow backend mechanisms to "abandon" exchanges

Introduce PG_SASL_EXCHANGE_ABANDONED, which allows CheckSASLAuth to
suppress the failing log entry for any SASL exchange that isn't actually
an authentication attempt. This is desirable for OAUTHBEARER's discovery
exchanges (and a subsequent commit will make use of it there).

This might have some overlap in the future with in-band aborts for SASL
exchanges, but it's intentionally not named _ABORTED to avoid confusion.
(We don't currently support clientside aborts in our SASL profile.)

Adapted from a patch by Zsolt Parragi.

Author: Zsolt Parragi <zsolt.parragi@percona.com>
Co-authored-by: Jacob Champion <jacob.champion@enterprisedb.com>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Discussion: https://postgr.es/m/CAN4CZFPim7hUiyb7daNKQPSZ8CvQRBGkVhbvED7yZi8VktSn4Q%40mail.gmail.com
This commit is contained in:
Jacob Champion 2026-03-31 11:47:31 -07:00
parent c2bca7cc96
commit c4ff16339f
3 changed files with 56 additions and 15 deletions

View file

@ -30,6 +30,12 @@
* be found for the role (or the user does not exist), and the mechanism
* should fail the authentication exchange.
*
* Some SASL mechanisms (e.g. OAUTHBEARER) define special exchanges for
* parameter discovery. These exchanges will always result in STATUS_ERROR,
* since we can't let the connection continue, but we shouldn't consider them to
* be failed authentication attempts. *abandoned will be set to true in this
* case.
*
* Mechanisms must take care not to reveal to the client that a user entry
* does not exist; ideally, the external failure mode is identical to that
* of an incorrect password. Mechanisms may instead use the logdetail
@ -42,7 +48,7 @@
*/
int
CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, char *shadow_pass,
const char **logdetail)
const char **logdetail, bool *abandoned)
{
StringInfoData sasl_mechs;
int mtype;
@ -167,7 +173,7 @@ CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, char *shadow_pass,
* PG_SASL_EXCHANGE_FAILURE with some output is forbidden by SASL.
* Make sure here that the mechanism used got that right.
*/
if (result == PG_SASL_EXCHANGE_FAILURE)
if (result == PG_SASL_EXCHANGE_FAILURE || result == PG_SASL_EXCHANGE_ABANDONED)
elog(ERROR, "output message found after SASL exchange failure");
/*
@ -184,6 +190,20 @@ CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, char *shadow_pass,
}
} while (result == PG_SASL_EXCHANGE_CONTINUE);
if (result == PG_SASL_EXCHANGE_ABANDONED)
{
if (!abandoned)
{
/*
* Programmer error: caller needs to track the abandoned state for
* this mechanism.
*/
elog(ERROR, "SASL exchange was abandoned, but CheckSASLAuth isn't tracking it");
}
*abandoned = true;
}
/* Oops, Something bad happened */
if (result != PG_SASL_EXCHANGE_SUCCESS)
{

View file

@ -45,7 +45,8 @@
* Global authentication functions
*----------------------------------------------------------------
*/
static void auth_failed(Port *port, int status, const char *logdetail);
static void auth_failed(Port *port, int elevel, int status,
const char *logdetail);
static char *recv_password_packet(Port *port);
@ -233,15 +234,18 @@ ClientAuthentication_hook_type ClientAuthentication_hook = NULL;
* anyway.
* Note that many sorts of failure report additional information in the
* postmaster log, which we hope is only readable by good guys. In
* particular, if logdetail isn't NULL, we send that string to the log.
* particular, if logdetail isn't NULL, we send that string to the log
* when the elevel allows.
*/
static void
auth_failed(Port *port, int status, const char *logdetail)
auth_failed(Port *port, int elevel, int status, const char *logdetail)
{
const char *errstr;
char *cdetail;
int errcode_return = ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION;
Assert(elevel >= FATAL); /* we must exit here */
/*
* If we failed due to EOF from client, just quit; there's no point in
* trying to send a message to the client, and not much point in logging
@ -314,12 +318,13 @@ auth_failed(Port *port, int status, const char *logdetail)
else
logdetail = cdetail;
ereport(FATAL,
ereport(elevel,
(errcode(errcode_return),
errmsg(errstr, port->user_name),
logdetail ? errdetail_log("%s", logdetail) : 0));
/* doesn't return */
pg_unreachable();
}
@ -381,6 +386,15 @@ ClientAuthentication(Port *port)
int status = STATUS_ERROR;
const char *logdetail = NULL;
/*
* "Abandoned" is a SASL-specific state similar to STATUS_EOF, in that we
* don't want to generate any server logs. But it's caused by an in-band
* client action that requires a server response, not an out-of-band
* connection closure, so we can't just proc_exit() like we do with
* STATUS_EOF.
*/
bool abandoned = false;
/*
* Get the authentication method to use for this frontend/database
* combination. Note: we do not parse the file at this point; this has
@ -625,7 +639,8 @@ ClientAuthentication(Port *port)
status = STATUS_OK;
break;
case uaOAuth:
status = CheckSASLAuth(&pg_be_oauth_mech, port, NULL, NULL);
status = CheckSASLAuth(&pg_be_oauth_mech, port, NULL, NULL,
&abandoned);
break;
}
@ -666,7 +681,10 @@ ClientAuthentication(Port *port)
if (status == STATUS_OK)
sendAuthRequest(port, AUTH_REQ_OK, NULL, 0);
else
auth_failed(port, status, logdetail);
auth_failed(port,
abandoned ? FATAL_CLIENT_ONLY : FATAL,
status,
logdetail);
}
@ -860,7 +878,7 @@ CheckPWChallengeAuth(Port *port, const char **logdetail)
auth_result = CheckMD5Auth(port, shadow_pass, logdetail);
else
auth_result = CheckSASLAuth(&pg_be_scram_mech, port, shadow_pass,
logdetail);
logdetail, NULL /* can't abandon SCRAM */ );
if (shadow_pass)
pfree(shadow_pass);

View file

@ -25,6 +25,7 @@
#define PG_SASL_EXCHANGE_CONTINUE 0
#define PG_SASL_EXCHANGE_SUCCESS 1
#define PG_SASL_EXCHANGE_FAILURE 2
#define PG_SASL_EXCHANGE_ABANDONED 3
/*
* Maximum accepted size of SASL messages.
@ -92,8 +93,8 @@ typedef struct pg_be_sasl_mech
*
* Produces a server challenge to be sent to the client. The callback
* must return one of the PG_SASL_EXCHANGE_* values, depending on
* whether the exchange continues, has finished successfully, or has
* failed.
* whether the exchange continues, has finished successfully, has
* failed, or was abandoned by the client.
*
* Input parameters:
*
@ -118,8 +119,9 @@ typedef struct pg_be_sasl_mech
* returned and the mechanism requires data to be sent during
* a successful outcome). The callback should set this to
* NULL if the exchange is over and no output should be sent,
* which should correspond to either PG_SASL_EXCHANGE_FAILURE
* or a PG_SASL_EXCHANGE_SUCCESS with no outcome data.
* which should correspond to either PG_SASL_EXCHANGE_FAILURE,
* PG_SASL_EXCHANGE_ABANDONED, or a PG_SASL_EXCHANGE_SUCCESS
* with no outcome data.
*
* outputlen: The length of the challenge data. Ignored if *output is
* NULL.
@ -128,7 +130,7 @@ typedef struct pg_be_sasl_mech
* server log, to disambiguate failure modes. (The client
* will only ever see the same generic authentication
* failure message.) Ignored if the exchange is completed
* with PG_SASL_EXCHANGE_SUCCESS.
* with PG_SASL_EXCHANGE_SUCCESS or PG_SASL_EXCHANGE_ABANDONED.
*---------
*/
int (*exchange) (void *state,
@ -142,6 +144,7 @@ typedef struct pg_be_sasl_mech
/* Common implementation for auth.c */
extern int CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port,
char *shadow_pass, const char **logdetail);
char *shadow_pass, const char **logdetail,
bool *abandoned);
#endif /* PG_SASL_H */