since-v9.diff.txt

text/plain

Filename: since-v9.diff.txt
Type: text/plain
Part: 0
Message: Re: [PoC] Federated Authn/z with OAUTHBEARER
1:  5f87f11b18e < -:  ----------- Add minor-version counterpart to (PG_)MAJORVERSION
2:  9e37fd7c217 ! 1:  e86e93f7ac8 oauth: Move the builtin flow into a separate module
    @@ Commit message
     
         The default flow relies on some libpq internals. Some of these can be
         safely duplicated (such as the SIGPIPE handlers), but others need to be
    -    shared between libpq and libpq-oauth for thread-safety. To avoid exporting
    -    these internals to all libpq clients forever, these dependencies are
    -    instead injected from the libpq side via an initialization function.
    -    This also lets libpq communicate the offset of conn->errorMessage to
    -    libpq-oauth, so that we can function without crashing if the module on
    -    the search path came from a different build of Postgres.
    +    shared between libpq and libpq-oauth for thread-safety. To avoid
    +    exporting these internals to all libpq clients forever, these
    +    dependencies are instead injected from the libpq side via an
    +    initialization function. This also lets libpq communicate the offsets of
    +    PGconn struct members to libpq-oauth, so that we can function without
    +    crashing if the module on the search path came from a different build of
    +    Postgres. (A minor-version upgrade could swap the libpq-oauth module out
    +    from under a long-running libpq client before it does its first load of
    +    the OAuth flow.)
     
         This ABI is considered "private". The module has no SONAME or version
    -    symlinks, and it's named libpq-oauth-<major>-<minor>.so to avoid mixing
    -    and matching across Postgres versions, in case internal struct order
    -    needs to change. (Future improvements may promote this "OAuth flow
    -    plugin" to a first-class concept, at which point we would need a public
    -    API to replace this anyway.)
    +    symlinks, and it's named libpq-oauth-<major>.so to avoid mixing and
    +    matching across Postgres versions. (Future improvements may promote this
    +    "OAuth flow plugin" to a first-class concept, at which point we would
    +    need a public API to replace this anyway.)
     
         Additionally, NLS support for error messages in b3f0be788a was
         incomplete, because the new error macros weren't being scanned by
    @@ src/interfaces/libpq-oauth/Makefile (new)
     +
     +# This is an internal module; we don't want an SONAME and therefore do not set
     +# SO_MAJOR_VERSION.
    -+NAME = pq-oauth-$(MAJORVERSION)-$(MINORVERSION)
    ++NAME = pq-oauth-$(MAJORVERSION)
     +
     +# Force the name "libpq-oauth" for both the static and shared libraries. The
     +# staticlib doesn't need version information in its name.
    @@ src/interfaces/libpq-oauth/README (new)
     += Load-Time ABI =
     +
     +This module ABI is an internal implementation detail, so it's subject to change
    -+across releases; the name of the module (libpq-oauth-MAJOR-MINOR) reflects this.
    ++across major releases; the name of the module (libpq-oauth-MAJOR) reflects this.
     +The module exports the following symbols:
     +
     +- PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn);
    @@ src/interfaces/libpq-oauth/README (new)
     +
     +- void libpq_oauth_init(pgthreadlock_t threadlock,
     +						libpq_gettext_func gettext_impl,
    -+						conn_errorMessage_func errmsg_impl);
    ++						conn_errorMessage_func errmsg_impl,
    ++						conn_oauth_client_id_func clientid_impl,
    ++						conn_oauth_client_secret_func clientsecret_impl,
    ++						conn_oauth_discovery_uri_func discoveryuri_impl,
    ++						conn_oauth_issuer_id_func issuerid_impl,
    ++						conn_oauth_scope_func scope_impl,
    ++						conn_sasl_state_func saslstate_impl,
    ++						set_conn_altsock_func setaltsock_impl,
    ++						set_conn_oauth_token_func settoken_impl);
     +
     +At the moment, pg_fe_run_oauth_flow() relies on libpq's pg_g_threadlock and
     +libpq_gettext(), which must be injected by libpq using this initialization
     +function before the flow is run.
     +
    -+It also relies on libpq to expose conn->errorMessage, via the errmsg_impl. This
    -+is done to decouple the module ABI from the offset of errorMessage, which can
    -+change positions depending on configure-time options. This way we can safely
    -+search the standard dlopen() paths (e.g. RPATH, LD_LIBRARY_PATH, the SO cache)
    -+for an implementation module to use, even if that module wasn't compiled at the
    -+same time as libpq.
    ++It also relies on access to several members of the PGconn struct. Not only can
    ++these change positions across minor versions, but the offsets aren't necessarily
    ++stable within a single minor release (conn->errorMessage, for instance, can
    ++change offsets depending on configure-time options). Therefore the necessary
    ++accessors (named conn_*) and mutators (set_conn_*) are injected here. With this
    ++approach, we can safely search the standard dlopen() paths (e.g. RPATH,
    ++LD_LIBRARY_PATH, the SO cache) for an implementation module to use, even if that
    ++module wasn't compiled at the same time as libpq -- which becomes especially
    ++important during "live upgrade" situations where a running libpq application has
    ++the libpq-oauth module updated out from under it before it's first loaded from
    ++disk.
     +
     += Static Build =
     +
     +The static library libpq.a does not perform any dynamic loading. If the builtin
     +flow is enabled, the application is expected to link against libpq-oauth.a
    -+directly to provide the necessary symbols.
    ++directly to provide the necessary symbols. (libpq.a and libpq-oauth.a must be
    ++part of the same build. Unlike the dynamic module, there are no translation
    ++shims provided.)
     
      ## src/interfaces/libpq-oauth/exports.txt (new) ##
     @@
    @@ src/interfaces/libpq-oauth/meson.build (new)
     +
     +# This is an internal module; we don't want an SONAME and therefore do not set
     +# SO_MAJOR_VERSION.
    -+libpq_oauth_name = 'libpq-oauth-@0@-@1@'.format(pg_version_major, pg_version_minor)
    ++libpq_oauth_name = 'libpq-oauth-@0@'.format(pg_version_major)
     +
     +libpq_oauth_so = shared_module(libpq_oauth_name,
     +  libpq_oauth_sources + libpq_oauth_so_sources,
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c => src/interfaces/libpq-oauth/oauth-cu
     -#include <unistd.h>
      
      #include "common/jsonapi.h"
    - #include "fe-auth.h"
    +-#include "fe-auth.h"
      #include "fe-auth-oauth.h"
     -#include "libpq-int.h"
      #include "mb/pg_wchar.h"
     +#include "oauth-curl.h"
    ++
     +#ifdef USE_DYNAMIC_OAUTH
    ++
    ++/*
    ++ * The module build is decoupled from libpq-int.h, to try to avoid inadvertent
    ++ * ABI breaks during minor version bumps. Replacements for the missing internals
    ++ * are provided by oauth-utils.
    ++ */
     +#include "oauth-utils.h"
    ++
    ++#else							/* !USE_DYNAMIC_OAUTH */
    ++
    ++/*
    ++ * Static builds may rely on PGconn offsets directly. Keep these aligned with
    ++ * the bank of callbacks in oauth-utils.h.
    ++ */
    ++#include "libpq-int.h"
    ++
    ++#define conn_errorMessage(CONN) (&CONN->errorMessage)
    ++#define conn_oauth_client_id(CONN) (CONN->oauth_client_id)
    ++#define conn_oauth_client_secret(CONN) (CONN->oauth_client_secret)
    ++#define conn_oauth_discovery_uri(CONN) (CONN->oauth_discovery_uri)
    ++#define conn_oauth_issuer_id(CONN) (CONN->oauth_issuer_id)
    ++#define conn_oauth_scope(CONN) (CONN->oauth_scope)
    ++#define conn_sasl_state(CONN) (CONN->sasl_state)
    ++
    ++#define set_conn_altsock(CONN, VAL) do { CONN->altsock = VAL; } while (0)
    ++#define set_conn_oauth_token(CONN, VAL) do { CONN->oauth_token = VAL; } while (0)
    ++
    ++#endif							/* !USE_DYNAMIC_OAUTH */
    ++
    ++/* One final guardrail against accidental inclusion... */
    ++#if defined(USE_DYNAMIC_OAUTH) && defined(LIBPQ_INT_H)
    ++#error do not rely on libpq-int.h in libpq-oauth.so
     +#endif
      
      /*
       * It's generally prudent to set a maximum response size to buffer in memory,
    +@@ src/interfaces/libpq-oauth/oauth-curl.c: free_async_ctx(PGconn *conn, struct async_ctx *actx)
    + void
    + pg_fe_cleanup_oauth_flow(PGconn *conn)
    + {
    +-	fe_oauth_state *state = conn->sasl_state;
    ++	fe_oauth_state *state = conn_sasl_state(conn);
    + 
    + 	if (state->async_ctx)
    + 	{
    +@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_cleanup_oauth_flow(PGconn *conn)
    + 		state->async_ctx = NULL;
    + 	}
    + 
    +-	conn->altsock = PGINVALID_SOCKET;
    ++	set_conn_altsock(conn, PGINVALID_SOCKET);
    + }
    + 
    + /*
     @@ src/interfaces/libpq-oauth/oauth-curl.c: parse_access_token(struct async_ctx *actx, struct token *tok)
      static bool
      setup_multiplexer(struct async_ctx *actx)
    @@ src/interfaces/libpq-oauth/oauth-curl.c: timer_expired(struct async_ctx *actx)
      }
      
      /*
    +@@ src/interfaces/libpq-oauth/oauth-curl.c: static bool
    + check_issuer(struct async_ctx *actx, PGconn *conn)
    + {
    + 	const struct provider *provider = &actx->provider;
    ++	const char *oauth_issuer_id = conn_oauth_issuer_id(conn);
    + 
    +-	Assert(conn->oauth_issuer_id);	/* ensured by setup_oauth_parameters() */
    ++	Assert(oauth_issuer_id);	/* ensured by setup_oauth_parameters() */
    + 	Assert(provider->issuer);	/* ensured by parse_provider() */
    + 
    + 	/*---
    +@@ src/interfaces/libpq-oauth/oauth-curl.c: check_issuer(struct async_ctx *actx, PGconn *conn)
    + 	 *    sent to. This comparison MUST use simple string comparison as defined
    + 	 *    in Section 6.2.1 of [RFC3986].
    + 	 */
    +-	if (strcmp(conn->oauth_issuer_id, provider->issuer) != 0)
    ++	if (strcmp(oauth_issuer_id, provider->issuer) != 0)
    + 	{
    + 		actx_error(actx,
    + 				   "the issuer identifier (%s) does not match oauth_issuer (%s)",
    +-				   provider->issuer, conn->oauth_issuer_id);
    ++				   provider->issuer, oauth_issuer_id);
    + 		return false;
    + 	}
    + 
    +@@ src/interfaces/libpq-oauth/oauth-curl.c: check_for_device_flow(struct async_ctx *actx)
    + static bool
    + add_client_identification(struct async_ctx *actx, PQExpBuffer reqbody, PGconn *conn)
    + {
    ++	const char *oauth_client_id = conn_oauth_client_id(conn);
    ++	const char *oauth_client_secret = conn_oauth_client_secret(conn);
    ++
    + 	bool		success = false;
    + 	char	   *username = NULL;
    + 	char	   *password = NULL;
    + 
    +-	if (conn->oauth_client_secret)	/* Zero-length secrets are permitted! */
    ++	if (oauth_client_secret)	/* Zero-length secrets are permitted! */
    + 	{
    + 		/*----
    + 		 * Use HTTP Basic auth to send the client_id and secret. Per RFC 6749,
    +@@ src/interfaces/libpq-oauth/oauth-curl.c: add_client_identification(struct async_ctx *actx, PQExpBuffer reqbody, PGconn *c
    + 		 * would it be redundant, but some providers in the wild (e.g. Okta)
    + 		 * refuse to accept it.
    + 		 */
    +-		username = urlencode(conn->oauth_client_id);
    +-		password = urlencode(conn->oauth_client_secret);
    ++		username = urlencode(oauth_client_id);
    ++		password = urlencode(oauth_client_secret);
    + 
    + 		if (!username || !password)
    + 		{
    +@@ src/interfaces/libpq-oauth/oauth-curl.c: add_client_identification(struct async_ctx *actx, PQExpBuffer reqbody, PGconn *c
    + 		 * If we're not otherwise authenticating, client_id is REQUIRED in the
    + 		 * request body.
    + 		 */
    +-		build_urlencoded(reqbody, "client_id", conn->oauth_client_id);
    ++		build_urlencoded(reqbody, "client_id", oauth_client_id);
    + 
    + 		CHECK_SETOPT(actx, CURLOPT_HTTPAUTH, CURLAUTH_NONE, goto cleanup);
    + 		actx->used_basic_auth = false;
    +@@ src/interfaces/libpq-oauth/oauth-curl.c: cleanup:
    + static bool
    + start_device_authz(struct async_ctx *actx, PGconn *conn)
    + {
    ++	const char *oauth_scope = conn_oauth_scope(conn);
    + 	const char *device_authz_uri = actx->provider.device_authorization_endpoint;
    + 	PQExpBuffer work_buffer = &actx->work_data;
    + 
    +-	Assert(conn->oauth_client_id);	/* ensured by setup_oauth_parameters() */
    ++	Assert(conn_oauth_client_id(conn)); /* ensured by setup_oauth_parameters() */
    + 	Assert(device_authz_uri);	/* ensured by check_for_device_flow() */
    + 
    + 	/* Construct our request body. */
    + 	resetPQExpBuffer(work_buffer);
    +-	if (conn->oauth_scope && conn->oauth_scope[0])
    +-		build_urlencoded(work_buffer, "scope", conn->oauth_scope);
    ++	if (oauth_scope && oauth_scope[0])
    ++		build_urlencoded(work_buffer, "scope", oauth_scope);
    + 
    + 	if (!add_client_identification(actx, work_buffer, conn))
    + 		return false;
    +@@ src/interfaces/libpq-oauth/oauth-curl.c: start_token_request(struct async_ctx *actx, PGconn *conn)
    + 	const char *device_code = actx->authz.device_code;
    + 	PQExpBuffer work_buffer = &actx->work_data;
    + 
    +-	Assert(conn->oauth_client_id);	/* ensured by setup_oauth_parameters() */
    ++	Assert(conn_oauth_client_id(conn)); /* ensured by setup_oauth_parameters() */
    + 	Assert(token_uri);			/* ensured by parse_provider() */
    + 	Assert(device_code);		/* ensured by parse_device_authz() */
    + 
     @@ src/interfaces/libpq-oauth/oauth-curl.c: prompt_user(struct async_ctx *actx, PGconn *conn)
      		.verification_uri_complete = actx->authz.verification_uri_complete,
      		.expires_in = actx->authz.expires_in,
    @@ src/interfaces/libpq-oauth/oauth-curl.c: prompt_user(struct async_ctx *actx, PGc
      
      	if (!res)
      	{
    -@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn)
    +@@ src/interfaces/libpq-oauth/oauth-curl.c: done:
    + static PostgresPollingStatusType
    + pg_fe_run_oauth_flow_impl(PGconn *conn)
      {
    - 	fe_oauth_state *state = conn->sasl_state;
    +-	fe_oauth_state *state = conn->sasl_state;
    ++	fe_oauth_state *state = conn_sasl_state(conn);
      	struct async_ctx *actx;
    ++	char	   *oauth_token = NULL;
     +	PQExpBuffer errbuf;
      
      	if (!initialize_curl(conn))
      		return PGRES_POLLING_FAILED;
     @@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn)
    + 	do
    + 	{
    + 		/* By default, the multiplexer is the altsock. Reassign as desired. */
    +-		conn->altsock = actx->mux;
    ++		set_conn_altsock(conn, actx->mux);
    + 
    + 		switch (actx->step)
    + 		{
    +@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn)
    + 				 */
    + 				if (!timer_expired(actx))
    + 				{
    +-					conn->altsock = actx->timerfd;
    ++					set_conn_altsock(conn, actx->timerfd);
    + 					return PGRES_POLLING_READING;
    + 				}
      
    - error_return:
    +@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn)
    + 		{
    + 			case OAUTH_STEP_INIT:
    + 				actx->errctx = "failed to fetch OpenID discovery document";
    +-				if (!start_discovery(actx, conn->oauth_discovery_uri))
    ++				if (!start_discovery(actx, conn_oauth_discovery_uri(conn)))
    + 					goto error_return;
      
    -+	/*
    -+	 * For the dynamic module build, we can't safely rely on the offset of
    -+	 * conn->errorMessage, since it depends on build options like USE_SSL et
    -+	 * al. libpq gives us a translator function instead.
    -+	 */
    -+#ifdef USE_DYNAMIC_OAUTH
    + 				actx->step = OAUTH_STEP_DISCOVERY;
    +@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn)
    + 				break;
    + 
    + 			case OAUTH_STEP_TOKEN_REQUEST:
    +-				if (!handle_token_response(actx, &conn->oauth_token))
    ++				if (!handle_token_response(actx, &oauth_token))
    + 					goto error_return;
    + 
    ++				/*
    ++				 * Hook any oauth_token into the PGconn immediately so that
    ++				 * the allocation isn't lost in case of an error.
    ++				 */
    ++				set_conn_oauth_token(conn, oauth_token);
    ++
    + 				if (!actx->user_prompted)
    + 				{
    + 					/*
    +@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn)
    + 					actx->user_prompted = true;
    + 				}
    + 
    +-				if (conn->oauth_token)
    ++				if (oauth_token)
    + 					break;		/* done! */
    + 
    + 				/*
    +@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn)
    + 				 * the client wait directly on the timerfd rather than the
    + 				 * multiplexer.
    + 				 */
    +-				conn->altsock = actx->timerfd;
    ++				set_conn_altsock(conn, actx->timerfd);
    + 
    + 				actx->step = OAUTH_STEP_WAIT_INTERVAL;
    + 				actx->running = 1;
    +@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn)
    + 		 * point, actx->running will be set. But there are some corner cases
    + 		 * where we can immediately loop back around; see start_request().
    + 		 */
    +-	} while (!conn->oauth_token && !actx->running);
    ++	} while (!oauth_token && !actx->running);
    + 
    + 	/* If we've stored a token, we're done. Otherwise come back later. */
    +-	return conn->oauth_token ? PGRES_POLLING_OK : PGRES_POLLING_READING;
    ++	return oauth_token ? PGRES_POLLING_OK : PGRES_POLLING_READING;
    + 
    + error_return:
     +	errbuf = conn_errorMessage(conn);
    -+#else
    -+	errbuf = &conn->errorMessage;
    -+#endif
    -+
    + 
      	/*
      	 * Assemble the three parts of our error: context, body, and detail. See
      	 * also the documentation for struct async_ctx.
    @@ src/interfaces/libpq-oauth/oauth-utils.c (new)
     +
     +#include <signal.h>
     +
    -+#include "libpq-int.h"
     +#include "oauth-utils.h"
     +
     +#ifndef USE_DYNAMIC_OAUTH
     +#error oauth-utils.c is not supported in static builds
     +#endif
     +
    -+static libpq_gettext_func libpq_gettext_impl;
    ++#ifdef LIBPQ_INT_H
    ++#error do not rely on libpq-int.h in libpq-oauth
    ++#endif
    ++
    ++/*
    ++ * Function pointers set by libpq_oauth_init().
    ++ */
     +
     +pgthreadlock_t pg_g_threadlock;
    ++static libpq_gettext_func libpq_gettext_impl;
    ++
     +conn_errorMessage_func conn_errorMessage;
    ++conn_oauth_client_id_func conn_oauth_client_id;
    ++conn_oauth_client_secret_func conn_oauth_client_secret;
    ++conn_oauth_discovery_uri_func conn_oauth_discovery_uri;
    ++conn_oauth_issuer_id_func conn_oauth_issuer_id;
    ++conn_oauth_scope_func conn_oauth_scope;
    ++conn_sasl_state_func conn_sasl_state;
    ++
    ++set_conn_altsock_func set_conn_altsock;
    ++set_conn_oauth_token_func set_conn_oauth_token;
     +
     +/*-
     + * Initializes libpq-oauth by setting necessary callbacks.
    @@ src/interfaces/libpq-oauth/oauth-utils.c (new)
     + *
     + * - libpq_gettext: translates error messages using libpq's message domain
     + *
    -+ * - conn->errorMessage: holds translated errors for the connection. This is
    -+ *   handled through a translation shim, which avoids either depending on the
    -+ *   offset of the errorMessage in PGconn, or needing to export the variadic
    -+ *   libpq_append_conn_error().
    ++ * The implementation also needs access to several members of the PGconn struct,
    ++ * which are not guaranteed to stay in place across minor versions. Accessors
    ++ * (named conn_*) and mutators (named set_conn_*) are injected here.
     + */
     +void
     +libpq_oauth_init(pgthreadlock_t threadlock_impl,
     +				 libpq_gettext_func gettext_impl,
    -+				 conn_errorMessage_func errmsg_impl)
    ++				 conn_errorMessage_func errmsg_impl,
    ++				 conn_oauth_client_id_func clientid_impl,
    ++				 conn_oauth_client_secret_func clientsecret_impl,
    ++				 conn_oauth_discovery_uri_func discoveryuri_impl,
    ++				 conn_oauth_issuer_id_func issuerid_impl,
    ++				 conn_oauth_scope_func scope_impl,
    ++				 conn_sasl_state_func saslstate_impl,
    ++				 set_conn_altsock_func setaltsock_impl,
    ++				 set_conn_oauth_token_func settoken_impl)
     +{
     +	pg_g_threadlock = threadlock_impl;
     +	libpq_gettext_impl = gettext_impl;
     +	conn_errorMessage = errmsg_impl;
    ++	conn_oauth_client_id = clientid_impl;
    ++	conn_oauth_client_secret = clientsecret_impl;
    ++	conn_oauth_discovery_uri = discoveryuri_impl;
    ++	conn_oauth_issuer_id = issuerid_impl;
    ++	conn_oauth_scope = scope_impl;
    ++	conn_sasl_state = saslstate_impl;
    ++	set_conn_altsock = setaltsock_impl;
    ++	set_conn_oauth_token = settoken_impl;
     +}
     +
     +/*
    @@ src/interfaces/libpq-oauth/oauth-utils.h (new)
     +#ifndef OAUTH_UTILS_H
     +#define OAUTH_UTILS_H
     +
    ++#include "fe-auth-oauth.h"
     +#include "libpq-fe.h"
     +#include "pqexpbuffer.h"
     +
    ++/*
    ++ * A bank of callbacks to safely access members of PGconn, which are all passed
    ++ * to libpq_oauth_init() by libpq.
    ++ *
    ++ * Keep these aligned with the definitions in fe-auth-oauth.c as well as the
    ++ * static declarations in oauth-curl.c.
    ++ */
    ++#define DECLARE_GETTER(TYPE, MEMBER) \
    ++	typedef TYPE (*conn_ ## MEMBER ## _func) (PGconn *conn); \
    ++	extern conn_ ## MEMBER ## _func conn_ ## MEMBER;
    ++
    ++#define DECLARE_SETTER(TYPE, MEMBER) \
    ++	typedef void (*set_conn_ ## MEMBER ## _func) (PGconn *conn, TYPE val); \
    ++	extern set_conn_ ## MEMBER ## _func set_conn_ ## MEMBER;
    ++
    ++DECLARE_GETTER(PQExpBuffer, errorMessage);
    ++DECLARE_GETTER(char *, oauth_client_id);
    ++DECLARE_GETTER(char *, oauth_client_secret);
    ++DECLARE_GETTER(char *, oauth_discovery_uri);
    ++DECLARE_GETTER(char *, oauth_issuer_id);
    ++DECLARE_GETTER(char *, oauth_scope);
    ++DECLARE_GETTER(fe_oauth_state *, sasl_state);
    ++
    ++DECLARE_SETTER(pgsocket, altsock);
    ++DECLARE_SETTER(char *, oauth_token);
    ++
    ++#undef DECLARE_GETTER
    ++#undef DECLARE_SETTER
    ++
     +typedef char *(*libpq_gettext_func) (const char *msgid);
    -+typedef PQExpBuffer (*conn_errorMessage_func) (PGconn *conn);
     +
     +/* Initializes libpq-oauth. */
     +extern PGDLLEXPORT void libpq_oauth_init(pgthreadlock_t threadlock,
     +										 libpq_gettext_func gettext_impl,
    -+										 conn_errorMessage_func errmsg_impl);
    ++										 conn_errorMessage_func errmsg_impl,
    ++										 conn_oauth_client_id_func clientid_impl,
    ++										 conn_oauth_client_secret_func clientsecret_impl,
    ++										 conn_oauth_discovery_uri_func discoveryuri_impl,
    ++										 conn_oauth_issuer_id_func issuerid_impl,
    ++										 conn_oauth_scope_func scope_impl,
    ++										 conn_sasl_state_func saslstate_impl,
    ++										 set_conn_altsock_func setaltsock_impl,
    ++										 set_conn_oauth_token_func settoken_impl);
     +
    -+/* Callback to safely obtain conn->errorMessage from a PGconn. */
    -+extern conn_errorMessage_func conn_errorMessage;
    ++/*
    ++ * Duplicated APIs, copied from libpq (primarily libpq-int.h, which we cannot
    ++ * depend on here).
    ++ */
    ++
    ++typedef enum
    ++{
    ++	PG_BOOL_UNKNOWN = 0,		/* Currently unknown */
    ++	PG_BOOL_YES,				/* Yes (true) */
    ++	PG_BOOL_NO					/* No (false) */
    ++} PGTernaryBool;
     +
    -+/* Duplicated APIs, copied from libpq. */
     +extern void libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3);
     +extern bool oauth_unsafe_debugging_enabled(void);
     +extern int	pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending);
     +extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe);
     +
    ++#ifdef ENABLE_NLS
    ++extern char *libpq_gettext(const char *msgid) pg_attribute_format_arg(1);
    ++#else
    ++#define libpq_gettext(x) (x)
    ++#endif
    ++
    ++extern pgthreadlock_t pg_g_threadlock;
    ++
    ++#define pglock_thread()		pg_g_threadlock(true)
    ++#define pgunlock_thread()	pg_g_threadlock(false)
    ++
     +#endif							/* OAUTH_UTILS_H */
     
      ## src/interfaces/libpq/Makefile ##
    @@ src/interfaces/libpq/fe-auth-oauth.c: cleanup_user_oauth_flow(PGconn *conn)
     + */
     +
     +typedef char *(*libpq_gettext_func) (const char *msgid);
    -+typedef PQExpBuffer (*conn_errorMessage_func) (PGconn *conn);
     +
     +/*
    -+ * This shim is injected into libpq-oauth so that it doesn't depend on the
    -+ * offset of conn->errorMessage.
    -+ *
    -+ * TODO: look into exporting libpq_append_conn_error or a comparable API from
    -+ * libpq, instead.
    ++ * Define accessor/mutator shims to inject into libpq-oauth, so that it doesn't
    ++ * depend on the offsets within PGconn. (These have changed during minor version
    ++ * updates in the past.)
     + */
    -+static PQExpBuffer
    -+conn_errorMessage(PGconn *conn)
    -+{
    -+	return &conn->errorMessage;
    -+}
    ++
    ++#define DEFINE_GETTER(TYPE, MEMBER) \
    ++	typedef TYPE (*conn_ ## MEMBER ## _func) (PGconn *conn); \
    ++	static TYPE conn_ ## MEMBER(PGconn *conn) { return conn->MEMBER; }
    ++
    ++/* Like DEFINE_GETTER, but returns a pointer to the member. */
    ++#define DEFINE_GETTER_P(TYPE, MEMBER) \
    ++	typedef TYPE (*conn_ ## MEMBER ## _func) (PGconn *conn); \
    ++	static TYPE conn_ ## MEMBER(PGconn *conn) { return &conn->MEMBER; }
    ++
    ++#define DEFINE_SETTER(TYPE, MEMBER) \
    ++	typedef void (*set_conn_ ## MEMBER ## _func) (PGconn *conn, TYPE val); \
    ++	static void set_conn_ ## MEMBER(PGconn *conn, TYPE val) { conn->MEMBER = val; }
    ++
    ++DEFINE_GETTER_P(PQExpBuffer, errorMessage);
    ++DEFINE_GETTER(char *, oauth_client_id);
    ++DEFINE_GETTER(char *, oauth_client_secret);
    ++DEFINE_GETTER(char *, oauth_discovery_uri);
    ++DEFINE_GETTER(char *, oauth_issuer_id);
    ++DEFINE_GETTER(char *, oauth_scope);
    ++DEFINE_GETTER(fe_oauth_state *, sasl_state);
    ++
    ++DEFINE_SETTER(pgsocket, altsock);
    ++DEFINE_SETTER(char *, oauth_token);
     +
     +/*
     + * Loads the libpq-oauth plugin via dlopen(), initializes it, and plugs its
    @@ src/interfaces/libpq/fe-auth-oauth.c: cleanup_user_oauth_flow(PGconn *conn)
     +
     +	void		(*init) (pgthreadlock_t threadlock,
     +						 libpq_gettext_func gettext_impl,
    -+						 conn_errorMessage_func errmsg_impl);
    ++						 conn_errorMessage_func errmsg_impl,
    ++						 conn_oauth_client_id_func clientid_impl,
    ++						 conn_oauth_client_secret_func clientsecret_impl,
    ++						 conn_oauth_discovery_uri_func discoveryuri_impl,
    ++						 conn_oauth_issuer_id_func issuerid_impl,
    ++						 conn_oauth_scope_func scope_impl,
    ++						 conn_sasl_state_func saslstate_impl,
    ++						 set_conn_altsock_func setaltsock_impl,
    ++						 set_conn_oauth_token_func settoken_impl);
     +	PostgresPollingStatusType (*flow) (PGconn *conn);
     +	void		(*cleanup) (PGconn *conn);
     +
    @@ src/interfaces/libpq/fe-auth-oauth.c: cleanup_user_oauth_flow(PGconn *conn)
     +	 */
     +	const char *const module_name =
     +#if defined(__darwin__)
    -+		LIBDIR "/libpq-oauth-" PG_MAJORVERSION "-" PG_MINORVERSION DLSUFFIX;
    ++		LIBDIR "/libpq-oauth-" PG_MAJORVERSION DLSUFFIX;
     +#else
    -+		"libpq-oauth-" PG_MAJORVERSION "-" PG_MINORVERSION DLSUFFIX;
    ++		"libpq-oauth-" PG_MAJORVERSION DLSUFFIX;
     +#endif
     +
     +	state->builtin_flow = dlopen(module_name, RTLD_NOW | RTLD_LOCAL);
    @@ src/interfaces/libpq/fe-auth-oauth.c: cleanup_user_oauth_flow(PGconn *conn)
     +#else
     +			 NULL,
     +#endif
    -+			 conn_errorMessage);
    ++			 conn_errorMessage,
    ++			 conn_oauth_client_id,
    ++			 conn_oauth_client_secret,
    ++			 conn_oauth_discovery_uri,
    ++			 conn_oauth_issuer_id,
    ++			 conn_oauth_scope,
    ++			 conn_sasl_state,
    ++			 set_conn_altsock,
    ++			 set_conn_oauth_token);
     +
     +		initialized = true;
     +	}
    @@ src/interfaces/libpq/fe-auth-oauth.c: setup_token_request(PGconn *conn, fe_oauth
      	return true;
     
      ## src/interfaces/libpq/fe-auth-oauth.h ##
    -@@ src/interfaces/libpq/fe-auth-oauth.h: typedef struct
    +@@
    + #ifndef FE_AUTH_OAUTH_H
    + #define FE_AUTH_OAUTH_H
    + 
    ++#include "fe-auth-sasl.h"
    + #include "libpq-fe.h"
    +-#include "libpq-int.h"
    + 
    + 
    + enum fe_oauth_step
    +@@ src/interfaces/libpq/fe-auth-oauth.h: enum fe_oauth_step
    + 	FE_OAUTH_SERVER_ERROR,
    + };
    + 
    ++/*
    ++ * This struct is exported to the libpq-oauth module. If changes are needed
    ++ * during backports to stable branches, please keep ABI compatibility (no
    ++ * changes to existing members, add new members at the end, etc.).
    ++ */
    + typedef struct
    + {
    + 	enum fe_oauth_step step;
      
      	PGconn	   *conn;
      	void	   *async_ctx;