since-v11.diff.txt

text/plain

Filename: since-v11.diff.txt
Type: text/plain
Part: 0
Message: Re: [PoC] Federated Authn/z with OAUTHBEARER
1:  36409a76ce = 1:  0ff053cf31 common/jsonapi: support FRONTEND clients
2:  1356b729db ! 2:  26a341e1de libpq: add OAUTHBEARER SASL mechanism
    @@ Commit message
             Visit https://oauth.example.org/login and enter the code: FPQ2-M4BG
     
         The OAuth issuer must support device authorization. No other OAuth flows
    -    are currently implemented.
    +    are currently implemented (but clients may provide their own flows; see
    +    below).
     
         The client implementation requires either libcurl or libiddawc and their
         development headers. Pass `curl` or `iddawc` to --with-oauth/-Doauth
    @@ Commit message
     
         Thomas Munro wrote the kqueue() implementation for oauth-curl; thanks!
     
    +    = PQauthDataHook =
    +
    +    Clients may override two pieces of OAuth handling using the new
    +    PQsetAuthDataHook():
    +
    +    - PQAUTHDATA_PROMPT_OAUTH_DEVICE: replaces the default user prompt to
    +      standard error when using the builtin device authorization flow
    +
    +    - PQAUTHDATA_OAUTH_BEARER_TOKEN: replaces the entire OAuth flow with a
    +      custom asynchronous implementation
    +
    +    In general, a hook implementation should examine the incoming `type` to
    +    decide whether or not to handle a specific piece of authdata; if not, it
    +    should delegate to the previous hook in the chain (retrievable via
    +    PQgetAuthDataHook()). Otherwise, it should return an integer > 0 and
    +    follow the authdata-specific instructions. Returning an integer < 0
    +    signals an error condition and abandons the connection attempt.
    +
    +    == PQAUTHDATA_PROMPT_OAUTH_DEVICE ==
    +
    +    The hook should display the device prompt (URL + code) using whatever
    +    method it prefers.
    +
    +    == PQAUTHDATA_OAUTH_BEARER_TOKEN ==
    +
    +    The hook should either directly return a Bearer token for the current
    +    user/issuer/scope combination, if one is available without blocking, or
    +    else set up an asynchronous callback to retrieve one. See the
    +    documentation for PQoauthBearerRequest.
    +
         Several TODOs:
         - don't retry forever if the server won't accept our token
         - perform several sanity checks on the OAuth issuer's responses
         - handle cases where the client has been set up with an issuer and
           scope, but the Postgres server wants to use something different
         - improve error debuggability during the OAuth handshake
    -    - migrate JSON parsing to the new JSON_SEM_ACTION_FAILED API convention
         - fix libcurl initialization thread-safety
         - harden the libcurl flow implementation
    -    - figure out how to report the user code and URL without the notice
    -      processor
    +    - figure out pgsocket/int difference on Windows
         - ...and more.
     
      ## configure ##
    @@ src/interfaces/libpq/Makefile: endif
      SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi32 -lssl -lsocket -lnsl -lresolv -lintl -lm $(PTHREAD_LIBS), $(LIBS)) $(LDAP_LIBS_FE)
      endif
     
    + ## src/interfaces/libpq/exports.txt ##
    +@@ src/interfaces/libpq/exports.txt: PQclosePrepared           188
    + PQclosePortal             189
    + PQsendClosePrepared       190
    + PQsendClosePortal         191
    ++PQsetAuthDataHook         192
    ++PQgetAuthDataHook         193
    ++PQdefaultAuthDataHook     194
    +
      ## src/interfaces/libpq/fe-auth-oauth-curl.c (new) ##
     @@
     +/*-------------------------------------------------------------------------
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +#include <unistd.h>
     +
     +#include "common/jsonapi.h"
    ++#include "fe-auth.h"
     +#include "fe-auth-oauth.h"
     +#include "libpq-int.h"
     +#include "mb/pg_wchar.h"
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +	 */
     +	struct provider		provider;
     +	struct device_authz	authz;
    ++
    ++	bool		user_prompted;	/* have we already sent the authz prompt? */
     +};
     +
     +/*
    -+ * Exported function to free the async_ctx, which is stored directly on the
    -+ * PGconn. This is called during pqDropConnection() so that we don't leak
    -+ * resources even if PQconnectPoll() never calls us back.
    ++ * Frees the async_ctx, which is stored directly on the PGconn. This is called
    ++ * during pqDropConnection() so that we don't leak resources even if
    ++ * PQconnectPoll() never calls us back.
     + *
     + * TODO: we should probably call this at the end of a successful authentication,
     + * too, to proactively free up resources.
     + */
    -+void
    -+pg_fe_free_oauth_async_ctx(PGconn *conn, void *ctx)
    ++static void
    ++free_curl_async_ctx(PGconn *conn, void *ctx)
     +{
     +	struct async_ctx *actx = ctx;
     +
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +#endif
     +
     +		state->async_ctx = actx;
    ++		state->free_async_ctx = free_curl_async_ctx;
     +
     +		initPQExpBuffer(&actx->work_data);
     +		initPQExpBuffer(&actx->errbuf);
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +			if (!finish_token_request(actx, &tok))
     +				goto error_return;
     +
    -+			/*
    -+			 * Now that we know the token endpoint isn't broken, give the user
    -+			 * the login instructions.
    -+			 *
    -+			 * TODO: allow client applications to override this handling.
    -+			 */
    -+			fprintf(stderr, "Visit %s and enter the code: %s",
    -+					actx->authz.verification_uri, actx->authz.user_code);
    ++			if (!actx->user_prompted)
    ++			{
    ++				int			res;
    ++				PQpromptOAuthDevice prompt = {
    ++					.verification_uri = actx->authz.verification_uri,
    ++					.user_code = actx->authz.user_code,
    ++					/* TODO: optional fields */
    ++				};
    ++
    ++				/*
    ++				 * Now that we know the token endpoint isn't broken, give the
    ++				 * user the login instructions.
    ++				 */
    ++				res = PQauthDataHook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn,
    ++									 &prompt);
    ++
    ++				if (!res)
    ++				{
    ++					fprintf(stderr, "Visit %s and enter the code: %s",
    ++							prompt.verification_uri, prompt.user_code);
    ++				}
    ++				else if (res < 0)
    ++				{
    ++					actx_error(actx, "device prompt failed");
    ++					goto error_return;
    ++				}
    ++
    ++				actx->user_prompted = true;
    ++			}
     +
     +			if (tok.access_token)
     +			{
    @@ src/interfaces/libpq/fe-auth-oauth-iddawc.c (new)
     +
     +		if (!user_prompted)
     +		{
    ++			int			res;
    ++			PQpromptOAuthDevice prompt = {
    ++				.verification_uri = verification_uri,
    ++				.user_code = user_code,
    ++				/* TODO: optional fields */
    ++			};
    ++
     +			/*
     +			 * Now that we know the token endpoint isn't broken, give the user
     +			 * the login instructions.
     +			 */
    -+			pqInternalNotice(&conn->noticeHooks,
    -+							 "Visit %s and enter the code: %s",
    -+							 verification_uri, user_code);
    ++			res = PQauthDataHook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn,
    ++								 &prompt);
    ++
    ++			if (!res)
    ++			{
    ++				fprintf(stderr, "Visit %s and enter the code: %s",
    ++						prompt.verification_uri, prompt.user_code);
    ++			}
    ++			else if (res < 0)
    ++			{
    ++				appendPQExpBufferStr(&conn->errorMessage,
    ++									 libpq_gettext("device prompt failed\n"));
    ++				goto cleanup;
    ++			}
     +
     +			user_prompted = true;
     +		}
    @@ src/interfaces/libpq/fe-auth-oauth-iddawc.c (new)
     +	/* TODO: actually make this asynchronous */
     +	state->token = run_iddawc_auth_flow(conn, conn->oauth_discovery_uri);
     +	return state->token ? PGRES_POLLING_OK : PGRES_POLLING_FAILED;
    -+}
    -+
    -+void
    -+pg_fe_free_oauth_async_ctx(PGconn *conn, void *ctx)
    -+{
    -+	/* We currently have no async_ctx, so this should not be called. */
    -+	Assert(false);
     +}
     
      ## src/interfaces/libpq/fe-auth-oauth.c (new) ##
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	Assert(sasl_mechanism != NULL);
     +	Assert(!strcmp(sasl_mechanism, OAUTHBEARER_NAME));
     +
    -+	state = malloc(sizeof(*state));
    ++	state = calloc(1, sizeof(*state));
     +	if (!state)
     +		return NULL;
     +
     +	state->state = FE_OAUTH_INIT;
     +	state->conn = conn;
    -+	state->token = NULL;
    -+	state->async_ctx = NULL;
     +
     +	return state;
     +}
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +{
     +	struct json_ctx	   *ctx = state;
     +
    -+	if (oauth_json_has_error(ctx))
    -+		return JSON_SUCCESS; /* short-circuit */
    -+
     +	if (ctx->target_field)
     +	{
     +		Assert(ctx->nested == 1);
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	}
     +
     +	++ctx->nested;
    -+	return JSON_SUCCESS; /* TODO: switch all of these to JSON_SEM_ACTION_FAILED */
    ++	return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS;
     +}
     +
     +static JsonParseErrorType
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +{
     +	struct json_ctx	   *ctx = state;
     +
    -+	if (oauth_json_has_error(ctx))
    -+		return JSON_SUCCESS; /* short-circuit */
    -+
     +	--ctx->nested;
     +	return JSON_SUCCESS;
     +}
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +{
     +	struct json_ctx	   *ctx = state;
     +
    -+	if (oauth_json_has_error(ctx))
    -+	{
    -+		/* short-circuit */
    -+		free(name);
    -+		return JSON_SUCCESS;
    -+	}
    -+
     +	if (ctx->nested == 1)
     +	{
     +		if (!strcmp(name, ERROR_STATUS_FIELD))
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +{
     +	struct json_ctx	   *ctx = state;
     +
    -+	if (oauth_json_has_error(ctx))
    -+		return JSON_SUCCESS; /* short-circuit */
    -+
     +	if (!ctx->nested)
     +	{
     +		ctx->errmsg = libpq_gettext("top-level element must be an object");
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +							 ctx->target_field_name);
     +	}
     +
    -+	return JSON_SUCCESS;
    ++	return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS;
     +}
     +
     +static JsonParseErrorType
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +{
     +	struct json_ctx	   *ctx = state;
     +
    -+	if (oauth_json_has_error(ctx))
    -+	{
    -+		/* short-circuit */
    -+		free(token);
    -+		return JSON_SUCCESS;
    -+	}
    -+
     +	if (!ctx->nested)
     +	{
     +		ctx->errmsg = libpq_gettext("top-level element must be an object");
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	}
     +
     +	free(token);
    -+	return JSON_SUCCESS;
    ++	return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS;
     +}
     +
     +static bool
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +
     +	err = pg_parse_json(&lex, &sem);
     +
    -+	if (err != JSON_SUCCESS)
    -+	{
    -+		errmsg = json_errdetail(err, &lex);
    -+	}
    -+	else if (PQExpBufferDataBroken(ctx.errbuf))
    -+	{
    -+		errmsg = libpq_gettext("out of memory");
    -+	}
    -+	else if (ctx.errmsg)
    ++	if (err == JSON_SEM_ACTION_FAILED)
     +	{
    -+		errmsg = ctx.errmsg;
    ++		if (PQExpBufferDataBroken(ctx.errbuf))
    ++			errmsg = libpq_gettext("out of memory");
    ++		else if (ctx.errmsg)
    ++			errmsg = ctx.errmsg;
    ++		else
    ++		{
    ++			/*
    ++			 * Developer error: one of the action callbacks didn't call
    ++			 * oauth_json_set_error() before erroring out.
    ++			 */
    ++			Assert(oauth_json_has_error(&ctx));
    ++			errmsg = "<unexpected empty error>";
    ++		}
     +	}
    ++	else if (err != JSON_SUCCESS)
    ++		errmsg = json_errdetail(err, &lex);
     +
     +	if (errmsg)
     +		appendPQExpBuffer(&conn->errorMessage,
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	return true;
     +}
     +
    ++static void
    ++free_request(PGconn *conn, void *vreq)
    ++{
    ++	PQoauthBearerRequest *request = vreq;
    ++
    ++	if (request->cleanup)
    ++		request->cleanup(conn, request);
    ++
    ++	free(request);
    ++}
    ++
    ++static PostgresPollingStatusType
    ++run_user_oauth_flow(PGconn *conn, pgsocket *altsock)
    ++{
    ++	fe_oauth_state *state = conn->sasl_state;
    ++	PQoauthBearerRequest *request = state->async_ctx;
    ++	PostgresPollingStatusType status;
    ++
    ++	if (!request->async)
    ++	{
    ++		libpq_append_conn_error(conn, "user-defined OAuth flow provided neither a token nor an async callback");
    ++		return PGRES_POLLING_FAILED;
    ++	}
    ++
    ++	status = request->async(conn, request, altsock);
    ++	if (status == PGRES_POLLING_FAILED)
    ++	{
    ++		libpq_append_conn_error(conn, "user-defined OAuth flow failed");
    ++		return status;
    ++	}
    ++	else if (status == PGRES_POLLING_OK)
    ++	{
    ++		/*
    ++		 * We already have a token, so copy it into the state. (We can't
    ++		 * hold onto the original string, since it may not be safe for us to
    ++		 * free() it.)
    ++		 */
    ++		PQExpBufferData	token;
    ++
    ++		if (!request->token)
    ++		{
    ++			libpq_append_conn_error(conn, "user-defined OAuth flow did not provide a token");
    ++			return PGRES_POLLING_FAILED;
    ++		}
    ++
    ++		initPQExpBuffer(&token);
    ++		appendPQExpBuffer(&token, "Bearer %s", request->token);
    ++
    ++		if (PQExpBufferDataBroken(token))
    ++		{
    ++			libpq_append_conn_error(conn, "out of memory");
    ++			return PGRES_POLLING_FAILED;
    ++		}
    ++
    ++		state->token = token.data;
    ++		return PGRES_POLLING_OK;
    ++	}
    ++
    ++	/* TODO: what if no altsock was set? */
    ++	return status;
    ++}
    ++
    ++static bool
    ++setup_token_request(PGconn *conn, fe_oauth_state *state)
    ++{
    ++	int			res;
    ++	PQoauthBearerRequest request = {
    ++		.openid_configuration = conn->oauth_discovery_uri,
    ++		.scope = conn->oauth_scope,
    ++	};
    ++
    ++	Assert(request.openid_configuration);
    ++
    ++	/* The client may have overridden the OAuth flow. */
    ++	res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN, conn, &request);
    ++	if (res > 0)
    ++	{
    ++		PQoauthBearerRequest *request_copy;
    ++
    ++		if (request.token)
    ++		{
    ++			/*
    ++			 * We already have a token, so copy it into the state. (We can't
    ++			 * hold onto the original string, since it may not be safe for us to
    ++			 * free() it.)
    ++			 */
    ++			PQExpBufferData	token;
    ++
    ++			initPQExpBuffer(&token);
    ++			appendPQExpBuffer(&token, "Bearer %s", request.token);
    ++
    ++			if (PQExpBufferDataBroken(token))
    ++			{
    ++				libpq_append_conn_error(conn, "out of memory");
    ++				goto fail;
    ++			}
    ++
    ++			state->token = token.data;
    ++
    ++			/* short-circuit */
    ++			if (request.cleanup)
    ++				request.cleanup(conn, &request);
    ++			return true;
    ++		}
    ++
    ++		request_copy = malloc(sizeof(*request_copy));
    ++		if (!request_copy)
    ++		{
    ++			libpq_append_conn_error(conn, "out of memory");
    ++			goto fail;
    ++		}
    ++
    ++		memcpy(request_copy, &request, sizeof(request));
    ++
    ++		conn->async_auth = run_user_oauth_flow;
    ++		state->async_ctx = request_copy;
    ++		state->free_async_ctx = free_request;
    ++	}
    ++	else if (res < 0)
    ++	{
    ++		libpq_append_conn_error(conn, "user-defined OAuth flow failed");
    ++		goto fail;
    ++	}
    ++	else
    ++	{
    ++		/* Use our built-in OAuth flow. */
    ++		conn->async_auth = pg_fe_run_oauth_flow;
    ++	}
    ++
    ++	return true;
    ++
    ++fail:
    ++	if (request.cleanup)
    ++		request.cleanup(conn, &request);
    ++	return false;
    ++}
    ++
     +static bool
     +derive_discovery_uri(PGconn *conn)
     +{
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +				}
     +
     +				/*
    -+				 * At this point we have to hand the connection over to our
    -+				 * OAuth implementation. This involves a number of HTTP
    -+				 * connections and timed waits, so we escape the synchronous
    -+				 * auth processing and tell PQconnectPoll to transfer control to
    -+				 * our async implementation.
    ++				 * Decide whether we're using a user-provided OAuth flow, or the
    ++				 * one we have built in.
     +				 */
    -+				conn->async_auth = pg_fe_run_oauth_flow;
    -+				state->state = FE_OAUTH_REQUESTING_TOKEN;
    -+				return SASL_ASYNC;
    -+			}
    ++				if (!setup_token_request(conn, state))
    ++					return SASL_FAILED;
     +
    -+			/*
    -+			 * If we don't have a discovery URI to be able to request a token,
    -+			 * we ask the server for one explicitly with an empty token. This
    -+			 * doesn't require any asynchronous work.
    -+			 */
    -+			state->token = strdup("");
    -+			if (!state->token)
    ++				if (state->token)
    ++				{
    ++					/*
    ++					 * A really smart user implementation may have already given
    ++					 * us the token (e.g. if there was an unexpired copy already
    ++					 * cached). In that case, we can just fall through.
    ++					 */
    ++				}
    ++				else
    ++				{
    ++					/*
    ++					 * Otherwise, we have to hand the connection over to our
    ++					 * OAuth implementation. This involves a number of HTTP
    ++					 * connections and timed waits, so we escape the synchronous
    ++					 * auth processing and tell PQconnectPoll to transfer
    ++					 * control to our async implementation.
    ++					 */
    ++					Assert(conn->async_auth); /* should have been set already */
    ++					state->state = FE_OAUTH_REQUESTING_TOKEN;
    ++					return SASL_ASYNC;
    ++				}
    ++			}
    ++			else
     +			{
    -+				libpq_append_conn_error(conn, "out of memory");
    -+				return SASL_FAILED;
    ++				/*
    ++				 * If we don't have a discovery URI to be able to request a
    ++				 * token, we ask the server for one explicitly with an empty
    ++				 * token. This doesn't require any asynchronous work.
    ++				 */
    ++				state->token = strdup("");
    ++				if (!state->token)
    ++				{
    ++					libpq_append_conn_error(conn, "out of memory");
    ++					return SASL_FAILED;
    ++				}
     +			}
     +
     +			/* fall through */
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +
     +	free(state->token);
     +	if (state->async_ctx)
    -+		pg_fe_free_oauth_async_ctx(state->conn, state->async_ctx);
    ++		state->free_async_ctx(state->conn, state->async_ctx);
     +
     +	free(state);
     +}
    @@ src/interfaces/libpq/fe-auth-oauth.h (new)
     +
     +	PGconn	   *conn;
     +	char	   *token;
    ++
     +	void	   *async_ctx;
    ++	void	  (*free_async_ctx) (PGconn *conn, void *ctx);
     +} fe_oauth_state;
     +
     +extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock);
    -+extern void pg_fe_free_oauth_async_ctx(PGconn *conn, void *ctx);
     +
     +#endif							/* FE_AUTH_OAUTH_H */
     
    @@ src/interfaces/libpq/fe-auth.c: pg_fe_sendauth(AuthRequest areq, int payloadlen,
      			{
      				/* Use this message if pg_SASL_continue didn't supply one */
      				if (conn->errorMessage.len == oldmsglen)
    +@@ src/interfaces/libpq/fe-auth.c: PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user,
    + 
    + 	return crypt_pwd;
    + }
    ++
    ++PQauthDataHook_type PQauthDataHook = PQdefaultAuthDataHook;
    ++
    ++PQauthDataHook_type
    ++PQgetAuthDataHook(void)
    ++{
    ++	return PQauthDataHook;
    ++}
    ++
    ++void
    ++PQsetAuthDataHook(PQauthDataHook_type hook)
    ++{
    ++	PQauthDataHook = hook ? hook : PQdefaultAuthDataHook;
    ++}
    ++
    ++int
    ++PQdefaultAuthDataHook(PGAuthData type, PGconn *conn, void *data)
    ++{
    ++	return 0; /* handle nothing */
    ++}
     
      ## src/interfaces/libpq/fe-auth.h ##
     @@
    + #include "libpq-int.h"
      
      
    ++extern PQauthDataHook_type PQauthDataHook;
    ++
    ++
      /* Prototypes for functions in fe-auth.c */
     -extern int	pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn);
     +extern int	pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn,
    @@ src/interfaces/libpq/fe-connect.c: keep_going:						/* We will come back to here
     +		case CONNECTION_AUTHENTICATING:
     +			{
     +				PostgresPollingStatusType status;
    -+				pgsocket	altsock;
    ++				pgsocket	altsock = PGINVALID_SOCKET;
     +
     +				if (!conn->async_auth)
     +				{
    @@ src/interfaces/libpq/fe-misc.c: pqSocketCheck(PGconn *conn, int forRead, int for
      	if (result < 0)
     
      ## src/interfaces/libpq/libpq-fe.h ##
    +@@ src/interfaces/libpq/libpq-fe.h: extern "C"
    + #define LIBPQ_HAS_TRACE_FLAGS 1
    + /* Indicates that PQsslAttribute(NULL, "library") is useful */
    + #define LIBPQ_HAS_SSL_LIBRARY_DETECTION 1
    ++/* Indicates presence of the PQAUTHDATA_PROMPT_OAUTH_DEVICE authdata hook */
    ++#define LIBPQ_HAS_PROMPT_OAUTH_DEVICE 1
    + 
    + /*
    +  * Option flags for PQcopyResult
     @@ src/interfaces/libpq/libpq-fe.h: typedef enum
      	CONNECTION_CONSUME,			/* Consuming any extra messages. */
      	CONNECTION_GSS_STARTUP,		/* Negotiating GSSAPI. */
    @@ src/interfaces/libpq/libpq-fe.h: typedef enum
      } ConnStatusType;
      
      typedef enum
    +@@ src/interfaces/libpq/libpq-fe.h: typedef enum
    + 	PQ_PIPELINE_ABORTED
    + } PGpipelineStatus;
    + 
    ++typedef enum
    ++{
    ++	PQAUTHDATA_PROMPT_OAUTH_DEVICE,	/* user must visit a device-authorization URL */
    ++	PQAUTHDATA_OAUTH_BEARER_TOKEN,	/* server requests an OAuth Bearer token */
    ++} PGAuthData;
    ++
    + /* PGconn encapsulates a connection to the backend.
    +  * The contents of this struct are not supposed to be known to applications.
    +  */
    +@@ src/interfaces/libpq/libpq-fe.h: extern int	PQenv2encoding(void);
    + 
    + /* === in fe-auth.c === */
    + 
    ++typedef struct _PQpromptOAuthDevice
    ++{
    ++	const char *verification_uri;			/* verification URI to visit */
    ++	const char *user_code;					/* user code to enter */
    ++} PQpromptOAuthDevice;
    ++
    ++typedef struct _PQoauthBearerRequest
    ++{
    ++	/* Hook inputs (constant across all calls) */
    ++	const char * const openid_configuration;	/* OIDC discovery URI */
    ++	const char * const scope;					/* required scope(s), or NULL */
    ++
    ++	/* Hook outputs */
    ++
    ++	/*
    ++	 * Callback implementing a custom asynchronous OAuth flow.
    ++	 *
    ++	 * The callback may return
    ++	 * - PGRES_POLLING_READING/WRITING, to indicate that a file descriptor has
    ++	 *   been stored in *altsock and libpq should wait until it is readable or
    ++	 *   writable before calling back;
    ++	 * - PGRES_POLLING_OK, to indicate that the flow is complete and
    ++	 *   request->token has been set; or
    ++	 * - PGRES_POLLING_FAILED, to indicate that token retrieval has failed.
    ++	 *
    ++	 * This callback is optional. If the token can be obtained without blocking
    ++	 * during the original call to the PQAUTHDATA_OAUTH_BEARER_TOKEN hook, it
    ++	 * may be returned directly, but one of request->async or request->token
    ++	 * must be set by the hook.
    ++	 */
    ++	PostgresPollingStatusType (*async) (PGconn *conn,
    ++										struct _PQoauthBearerRequest *request,
    ++										int *altsock);
    ++
    ++	/*
    ++	 * Callback to clean up custom allocations. A hook implementation may use
    ++	 * this to free request->token and any resources in request->user.
    ++	 *
    ++	 * This is technically optional, but highly recommended, because there is no
    ++	 * other indication as to when it is safe to free the token.
    ++	 */
    ++	void	  (*cleanup) (PGconn *conn, struct _PQoauthBearerRequest *request);
    ++
    ++	/*
    ++	 * The hook should set this to the Bearer token contents for the connection,
    ++	 * once the flow is completed.  The token contents must remain available to
    ++	 * libpq until the hook's cleanup callback is called.
    ++	 */
    ++	char	   *token;
    ++
    ++	/*
    ++	 * Hook-defined data. libpq will not modify this pointer across calls to the
    ++	 * async callback, so it can be used to keep track of application-specific
    ++	 * state. Resources allocated here should be freed by the cleanup callback.
    ++	 */
    ++	void	   *user;
    ++} PQoauthBearerRequest;
    ++
    + extern char *PQencryptPassword(const char *passwd, const char *user);
    + extern char *PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user, const char *algorithm);
    + 
    ++typedef int (*PQauthDataHook_type) (PGAuthData type, PGconn *conn, void *data);
    ++extern void	PQsetAuthDataHook(PQauthDataHook_type hook);
    ++extern PQauthDataHook_type PQgetAuthDataHook(void);
    ++extern int	PQdefaultAuthDataHook(PGAuthData type, PGconn *conn, void *data);
    ++
    + /* === in encnames.c === */
    + 
    + extern int	pg_char_to_encoding(const char *name);
     
      ## src/interfaces/libpq/libpq-int.h ##
     @@ src/interfaces/libpq/libpq-int.h: typedef struct pg_conn_host
3:  863a49f863 = 3:  d02bc9a466 backend: add OAUTHBEARER SASL mechanism
4:  348554e5f4 ! 4:  fded01d22b Add pytest suite for OAuth
    @@ src/test/python/client/test_oauth.py (new)
     +#
     +
     +import base64
    ++import collections
    ++import ctypes
     +import http.server
     +import json
    ++import logging
    ++import os
    ++import platform
     +import secrets
     +import sys
     +import threading
     +import time
    ++import traceback
    ++import types
     +import urllib.parse
     +from numbers import Number
     +
    @@ src/test/python/client/test_oauth.py (new)
     +
     +from .conftest import BLOCKING_TIMEOUT
     +
    ++if platform.system() == "Darwin":
    ++    libpq = ctypes.cdll.LoadLibrary("libpq.5.dylib")
    ++else:
    ++    libpq = ctypes.cdll.LoadLibrary("libpq.so.5")
    ++
     +
     +def finish_handshake(conn):
     +    """
    @@ src/test/python/client/test_oauth.py (new)
     +        thread.stop()
     +
     +
    ++#
    ++# PQAuthDataHook implementation, matching libpq.h
    ++#
    ++
    ++
    ++PQAUTHDATA_PROMPT_OAUTH_DEVICE = 0
    ++PQAUTHDATA_OAUTH_BEARER_TOKEN = 1
    ++
    ++PGRES_POLLING_FAILED = 0
    ++PGRES_POLLING_READING = 1
    ++PGRES_POLLING_WRITING = 2
    ++PGRES_POLLING_OK = 3
    ++
    ++
    ++class PQPromptOAuthDevice(ctypes.Structure):
    ++    _fields_ = [
    ++        ("verification_uri", ctypes.c_char_p),
    ++        ("user_code", ctypes.c_char_p),
    ++    ]
    ++
    ++
    ++class PQOAuthBearerRequest(ctypes.Structure):
    ++    pass
    ++
    ++
    ++PQOAuthBearerRequest._fields_ = [
    ++    ("openid_configuration", ctypes.c_char_p),
    ++    ("scope", ctypes.c_char_p),
    ++    (
    ++        "async_",
    ++        ctypes.CFUNCTYPE(
    ++            ctypes.c_int,
    ++            ctypes.c_void_p,
    ++            ctypes.POINTER(PQOAuthBearerRequest),
    ++            ctypes.POINTER(ctypes.c_int),
    ++        ),
    ++    ),
    ++    (
    ++        "cleanup",
    ++        ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(PQOAuthBearerRequest)),
    ++    ),
    ++    ("token", ctypes.c_char_p),
    ++    ("user", ctypes.c_void_p),
    ++]
    ++
    ++
    ++@pytest.fixture
    ++def auth_data_cb():
    ++    """
    ++    Tracks calls to the libpq authdata hook. The yielded object contains a calls
    ++    member that records the data sent to the hook. If a test needs to perform
    ++    custom actions during a call, it can set the yielded object's impl callback;
    ++    beware that the callback takes place on a different thread.
    ++
    ++    This is done differently from the other callback implementations on purpose.
    ++    For the others, we can declare test-specific callbacks and have them perform
    ++    direct assertions on the data they receive. But that won't work for a C
    ++    callback, because there's no way for us to bubble up the assertion through
    ++    libpq. Instead, this mock-style approach is taken, where we just record the
    ++    calls and let the test examine them later.
    ++    """
    ++
    ++    class _Call:
    ++        pass
    ++
    ++    class _cb(object):
    ++        def __init__(self):
    ++            self.calls = []
    ++
    ++    cb = _cb()
    ++    cb.impl = None
    ++
    ++    # The callback will occur on a different thread, so protect the cb object.
    ++    cb_lock = threading.Lock()
    ++
    ++    @ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_byte, ctypes.c_void_p, ctypes.c_void_p)
    ++    def auth_data_cb(typ, pgconn, data):
    ++        handle_by_default = 0  # does an implementation have to be provided?
    ++
    ++        if typ == PQAUTHDATA_PROMPT_OAUTH_DEVICE:
    ++            cls = PQPromptOAuthDevice
    ++            handle_by_default = 1
    ++        elif typ == PQAUTHDATA_OAUTH_BEARER_TOKEN:
    ++            cls = PQOAuthBearerRequest
    ++        else:
    ++            return 0
    ++
    ++        call = _Call()
    ++        call.type = typ
    ++
    ++        # The lifetime of the underlying data being pointed to doesn't
    ++        # necessarily match the lifetime of the Python object, so we can't
    ++        # reference a Structure's fields after returning. Explicitly copy the
    ++        # contents over, field by field.
    ++        data = ctypes.cast(data, ctypes.POINTER(cls))
    ++        for name, _ in cls._fields_:
    ++            setattr(call, name, getattr(data.contents, name))
    ++
    ++        with cb_lock:
    ++            cb.calls.append(call)
    ++
    ++        if cb.impl:
    ++            # Pass control back to the test.
    ++            try:
    ++                return cb.impl(typ, pgconn, data.contents)
    ++            except Exception:
    ++                # This can't escape into the C stack, but we can fail the flow
    ++                # and hope the traceback gives us enough detail.
    ++                logging.error(
    ++                    "Exception during authdata hook callback:\n"
    ++                    + traceback.format_exc()
    ++                )
    ++                return -1
    ++
    ++        return handle_by_default
    ++
    ++    libpq.PQsetAuthDataHook(auth_data_cb)
    ++    try:
    ++        yield cb
    ++    finally:
    ++        # The callback is about to go out of scope, so make sure libpq is
    ++        # disconnected from it. (We wouldn't want to accidentally influence
    ++        # later tests anyway.)
    ++        libpq.PQsetAuthDataHook(None)
    ++
    ++
     +@pytest.mark.parametrize("secret", [None, "", "hunter2"])
     +@pytest.mark.parametrize("scope", [None, "", "openid email"])
     +@pytest.mark.parametrize("retries", [0, 1])
    @@ src/test/python/client/test_oauth.py (new)
     +    ],
     +)
     +def test_oauth_with_explicit_issuer(
    -+    capfd, accept, openid_provider, asynchronous, retries, scope, secret
    ++    accept, openid_provider, asynchronous, retries, scope, secret, auth_data_cb
     +):
     +    client_id = secrets.token_hex()
     +
    @@ src/test/python/client/test_oauth.py (new)
     +            finish_handshake(conn)
     +
     +    if retries:
    -+        # Finally, make sure that the client prompted the user with the expected
    -+        # authorization URL and user code.
    -+        expected = f"Visit {verification_url} and enter the code: {user_code}"
    -+        _, stderr = capfd.readouterr()
    -+        assert expected in stderr
    ++        # Finally, make sure that the client prompted the user once with the
    ++        # expected authorization URL and user code.
    ++        assert len(auth_data_cb.calls) == 2
    ++
    ++        # First call should have been for a custom flow, which we ignored.
    ++        assert auth_data_cb.calls[0].type == PQAUTHDATA_OAUTH_BEARER_TOKEN
    ++
    ++        # Second call is for our user prompt.
    ++        call = auth_data_cb.calls[1]
    ++        assert call.type == PQAUTHDATA_PROMPT_OAUTH_DEVICE
    ++        assert call.verification_uri.decode() == verification_url
    ++        assert call.user_code.decode() == user_code
     +
     +
     +def expect_disconnected_handshake(sock):
    @@ src/test/python/client/test_oauth.py (new)
     +            finish_handshake(conn)
     +
     +
    ++@pytest.fixture
    ++def self_pipe():
    ++    """
    ++    Yields a pipe fd pair.
    ++    """
    ++
    ++    class _Pipe:
    ++        pass
    ++
    ++    p = _Pipe()
    ++    p.readfd, p.writefd = os.pipe()
    ++
    ++    try:
    ++        yield p
    ++    finally:
    ++        os.close(p.readfd)
    ++        os.close(p.writefd)
    ++
    ++
    ++@pytest.mark.parametrize("scope", [None, "", "openid email"])
    ++@pytest.mark.parametrize(
    ++    "retries",
    ++    [
    ++        -1,  # no async callback
    ++        0,  # async callback immediately returns token
    ++        1,  # async callback waits on altsock once
    ++        2,  # async callback waits on altsock twice
    ++    ],
    ++)
    ++@pytest.mark.parametrize(
    ++    "asynchronous",
    ++    [
    ++        pytest.param(False, id="synchronous"),
    ++        pytest.param(True, id="asynchronous"),
    ++    ],
    ++)
    ++def test_user_defined_flow(
    ++    accept, auth_data_cb, self_pipe, scope, retries, asynchronous
    ++):
    ++    issuer = "http://localhost"
    ++    discovery_uri = issuer + "/.well-known/openid-configuration"
    ++    access_token = secrets.token_urlsafe()
    ++
    ++    sock, _ = accept(
    ++        oauth_issuer=issuer,
    ++        oauth_client_id="some-id",
    ++        oauth_scope=scope,
    ++        async_=asynchronous,
    ++    )
    ++
    ++    # Track callbacks.
    ++    attempts = 0
    ++    wakeup_called = False
    ++    cleanup_calls = 0
    ++    lock = threading.Lock()
    ++
    ++    def wakeup():
    ++        """Writes a byte to the wakeup pipe."""
    ++        nonlocal wakeup_called
    ++        with lock:
    ++            wakeup_called = True
    ++            os.write(self_pipe.writefd, b"\0")
    ++
    ++    def get_token(pgconn, request, p_altsock):
    ++        """
    ++        Async token callback. While attempts < retries, libpq will be instructed
    ++        to wait on the self_pipe. When attempts == retries, the token will be
    ++        set.
    ++
    ++        Note that assertions and exceptions raised here are allowed but not very
    ++        helpful, since they can't bubble through the libpq stack to be collected
    ++        by the test suite. Try not to rely too heavily on them.
    ++        """
    ++        # Make sure libpq passed our user data through.
    ++        assert request.user == 42
    ++
    ++        with lock:
    ++            nonlocal attempts, wakeup_called
    ++
    ++            if attempts:
    ++                # If we've already started the timer, we shouldn't get a
    ++                # call back before it trips.
    ++                assert wakeup_called, "authdata hook was called before the timer"
    ++
    ++                # Drain the wakeup byte.
    ++                os.read(self_pipe.readfd, 1)
    ++
    ++            if attempts < retries:
    ++                attempts += 1
    ++
    ++                # Wake up the client in a little bit of time.
    ++                wakeup_called = False
    ++                threading.Timer(0.1, wakeup).start()
    ++
    ++                # Tell libpq to wait on the other end of the wakeup pipe.
    ++                p_altsock[0] = self_pipe.readfd
    ++                return PGRES_POLLING_READING
    ++
    ++        # Done!
    ++        request.token = access_token.encode()
    ++        return PGRES_POLLING_OK
    ++
    ++    @ctypes.CFUNCTYPE(
    ++        ctypes.c_int,
    ++        ctypes.c_void_p,
    ++        ctypes.POINTER(PQOAuthBearerRequest),
    ++        ctypes.POINTER(ctypes.c_int),
    ++    )
    ++    def get_token_wrapper(pgconn, p_request, p_altsock):
    ++        """
    ++        Translation layer between C and Python for the async callback.
    ++        Assertions and exceptions will be swallowed at the boundary, so make
    ++        sure they don't escape here.
    ++        """
    ++        try:
    ++            return get_token(pgconn, p_request.contents, p_altsock)
    ++        except Exception:
    ++            logging.error("Exception during async callback:\n" + traceback.format_exc())
    ++            return PGRES_POLLING_FAILED
    ++
    ++    @ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(PQOAuthBearerRequest))
    ++    def cleanup(pgconn, p_request):
    ++        """
    ++        Should be called exactly once per connection.
    ++        """
    ++        nonlocal cleanup_calls
    ++        with lock:
    ++            cleanup_calls += 1
    ++
    ++    def bearer_hook(typ, pgconn, request):
    ++        """
    ++        Implementation of the PQAuthDataHook, which either sets up an async
    ++        callback or returns the token directly, depending on the value of
    ++        retries.
    ++
    ++        As above, try not to rely too much on assertions/exceptions here.
    ++        """
    ++        assert typ == PQAUTHDATA_OAUTH_BEARER_TOKEN
    ++        request.cleanup = cleanup
    ++
    ++        if retries < 0:
    ++            # Special case: return a token immediately without a callback.
    ++            request.token = access_token.encode()
    ++            return 1
    ++
    ++        # Tell libpq to call us back.
    ++        request.async_ = get_token_wrapper
    ++        request.user = ctypes.c_void_p(42)  # will be checked in the callback
    ++        return 1
    ++
    ++    auth_data_cb.impl = bearer_hook
    ++
    ++    # Now drive the server side.
    ++    with sock:
    ++        with pq3.wrap(sock, debug_stream=sys.stdout) as conn:
    ++            # Initiate a handshake, which should result in our custom callback
    ++            # being invoked to fetch the token.
    ++            initial = start_oauth_handshake(conn)
    ++
    ++            # Validate and accept the token.
    ++            auth = get_auth_value(initial)
    ++            assert auth == f"Bearer {access_token}".encode("ascii")
    ++
    ++            pq3.send(conn, pq3.types.AuthnRequest, type=pq3.authn.SASLFinal)
    ++            finish_handshake(conn)
    ++
    ++    # Check the data provided to the hook.
    ++    assert len(auth_data_cb.calls) == 1
    ++
    ++    call = auth_data_cb.calls[0]
    ++    assert call.type == PQAUTHDATA_OAUTH_BEARER_TOKEN
    ++    assert call.openid_configuration.decode() == discovery_uri
    ++    assert call.scope == (None if scope is None else scope.encode())
    ++
    ++    # Make sure we cleaned up after ourselves.
    ++    assert cleanup_calls == 1
    ++
    ++
     +def alt_patterns(*patterns):
     +    """
     +    Just combines multiple alternative regexes into one. It's not very efficient
5:  16d3984a45 ! 5:  38a9691801 squash! Add pytest suite for OAuth
    @@ src/test/python/README: To make quick smoke tests possible, slow tests have been
     +    $ py.test --temp-instance=./tmp_check
     
      ## src/test/python/client/test_oauth.py ##
    -@@
    - import base64
    - import http.server
    - import json
    -+import os
    - import secrets
    - import sys
    - import threading
     @@ src/test/python/client/test_oauth.py: import pq3
      
      from .conftest import BLOCKING_TIMEOUT
    @@ src/test/python/client/test_oauth.py: import pq3
     +    reason="OAuth client tests require --with-oauth support",
     +)
     +
    - 
    - def finish_handshake(conn):
    -     """
    + if platform.system() == "Darwin":
    +     libpq = ctypes.cdll.LoadLibrary("libpq.5.dylib")
    + else:
     
      ## src/test/python/conftest.py ##
     @@ src/test/python/conftest.py: import os