Subject: Re: [PoC] Federated Authn/z with OAUTHBEARER

From: Christoph Berg <[email-masked]>
Date: Wed, 23 Apr 2025 17:39:08 +0200

Re: Jacob Champion
> - Per ABI comment upthread, we are back to major-minor versioning for
> the shared library (e.g. libpq-oauth-18-0.so). 0001 adds the macros
> and makefile variables to make this easy, and 0002 is the bulk of the
> change now.

This will cause problems when programs are running while packages are
updated on disk. That program then tries to dlopen 18-0.so when there
is already 18-1.so installed. Relevant when the first oauth connection
is made way after startup.

This is trading one problem for another, but within-a-major ABI
changes should be much rarer than normal minor updates with
applications restarting only later.

Alternatively, there could be a dedicated SONAME for the plugin that
only changes when necessary, but perhaps the simple "18.so" solution
is good enough.

Christoph

---

From: Jacob Champion <[email-masked]>
Date: Wed, 23 Apr 2025 09:07:52 -0700

> is made way after startup.

Ugh, good point. This hazard applies to the previous suggestion of
pkglibdir, too, but in that case it would have been silent...

> applications restarting only later.

But the consequences are much worse for a silent ABI mismatch. Imagine
if libpq-oauth examines the wrong pointer inside PGconn for a
security-critical check.

> only changes when necessary, but perhaps the simple "18.so" solution

I don't think SONAME helps us, does it? We're not using it in dlopen().

We could all agree to bump the second number in the filename whenever
there's an internal ABI change. That works from a technical
perspective, but it's hard to test and enforce and... just not forget.
Or, I may still be able to thread the needle with a fuller lookup
table, and remove the dependency on libpq-int.h; it's just not going
to be incredibly pretty. Thinking...

Thanks so much for your continued review!

--Jacob

---

From: Christoph Berg <[email-masked]>
Date: Wed, 23 Apr 2025 18:38:07 +0200

Re: Jacob Champion
> if libpq-oauth examines the wrong pointer inside PGconn for a

True.

> I don't think SONAME helps us, does it? We're not using it in dlopen().

That was paraphrasing, with SONAME I meant "library file name that
changes when the ABI changes".

> perspective, but it's hard to test and enforce and... just not forget.

It's hopefully not harder than checking ABI compatibility of any other
libpq change, just a different number. If that number is in the
meson.build in the same directory, people should be able to connect
the dots.

Btw, if we have that number, we might as well drop the MAJOR part as
well... apt.pg.o is always shipping the latest libpq5, so major libpq
upgrades while apps are running are going to happen. (But this is just
once a year and much less problematic than minor upgrades and I'm not
going to complain if MAJOR is kept.)

> to be incredibly pretty. Thinking...

Don't overdesign it...

Christoph

---

From: Jacob Champion <[email-masked]>
Date: Wed, 23 Apr 2025 10:46:14 -0700

> meson.build in the same directory, people should be able to connect

I think it is harder, simply because no one has to do it today, and
that change would sign them up to do it, forever, adding to the
backport checklist. It's one thing if there's a bunch of committers
who pile into the thread right now saying "yes, that's okay", but I
don't really feel comfortable making that decision for them right this
instant.

If we had robust ABI compatibility checks as part of the farm [1], I
think we could do that. Doesn't feel like an 18 thing, though.

> going to complain if MAJOR is kept.)

I don't want to introduce another testing matrix dimension if I can
avoid it. ("I have this bug where libpq.so.5.18 is using
libpq-oauth.so from PG20 and I had no idea it was doing that and the
problem went away when I restarted and...")

And the intent is for this to be temporary until we have a user-facing
API. If this is the solution we go with, I think it'd wise to prepare
for a -19 version of libpq-oauth, but I'm going to try my best to get
custom modules in ASAP. People are going to be annoyed that v1 of the
feature doesn't let them swap the flow for our utilities. Ideally they
only have to deal with that for a single major release.

Also: since the libpq-oauth-18 and libpq-oauth-19 packages can be
installed side-by-side safely, isn't the upgrade hazard significantly
diminished? (If a user uninstalls the previous libpq-oauth version
while they're still running that version of libpq in memory, _and_
they've somehow never used OAuth until right that instant... it's easy
enough for them to undo their mistake while the application is still
running.)

> > to be incredibly pretty. Thinking...

Oh, I agree... but my "minimal" ABI designs have all had corner cases
so far. I may need to just bite the bullet.

Are there any readers who feel like an internal ABI version for
`struct pg_conn`, bumped during breaking backports, would be
acceptable? (More definitively: are there any readers who would veto
that?) You're still signing up for delayed errors in the long-lived
client case, so it's not a magic bullet, but the breakage is easy to
see and it's not a crash. The client application "just" has to restart
after a libpq upgrade.

Thanks,
--Jacob

[1] https://www.postgresql.org/message-id/B142EE8A-5D38-48B9-A4BB-82D69A854B55%40justatheory.com

---

From: Jacob Champion <[email-masked]>
Date: Mon, 28 Apr 2025 17:10:06 -0700

On Wed, Apr 23, 2025 at 10:46 AM Jacob Champion
<[email-masked]> wrote:
> that?)

To keep things moving: I assume this is unacceptable. So v10 redirects
every access to a PGconn struct member through a shim, similarly to
how conn->errorMessage was translated in v9. This adds plenty of new
boilerplate, but not a whole lot of complexity. To try to keep us
honest, libpq-int.h has been removed from the libpq-oauth includes.

This will now handle in-place minor version upgrades that swap pg_conn
internals around, so I've gone back to -MAJOR versioning alone.
fe_oauth_state is still exported; it now has an ABI warning above it.
(I figure that's easier to draw a line around during backports,
compared to everything in PGconn. We can still break things there
during major version upgrades.)

Thanks,
--Jacob
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;