On Sat, Apr 19, 2025 at 5:04 AM Christoph Berg <m...@debian.org> wrote:
> How about this:
>
>   No libpq OAuth flows are available. (Try installing the libpq-oauth 
> package.)

Tweaked for capitalization/punctuation rules, and removing the first
"libpq" mention (which I don't think helps a user of, say, psql):

    no OAuth flows are available (try installing the libpq-oauth package)

v8 also makes the following changes:

- 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.
- Since libpq-oauth.a is going to be discovered at compile time, not
runtime, I've removed the versioning from that filename. Static
clients need to match them anyway, so we don't need that additional
packaging headache.
- conn->errorMessage is now decoupled from oauth-curl.c. Separate
object file builds are made using the same technique as libpq.

Thanks,
--Jacob
-:  ----------- > 1:  5f87f11b18e Add minor-version counterpart to 
(PG_)MAJORVERSION
1:  942ad5391e2 ! 2:  4c9cc7f69af oauth: Move the builtin flow into a separate 
module
    @@ Commit message
         the search path came from a different build of Postgres.
     
         This ABI is considered "private". The module has no SONAME or version
    -    symlinks, and it's named libpq-oauth-<major>.so to avoid mixing and
    -    matching across major 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.)
    +    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.)
     
         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)
    ++NAME = pq-oauth-$(MAJORVERSION)-$(MINORVERSION)
     +
    -+# Force the name "libpq-oauth" for both the static and shared libraries.
    ++# Force the name "libpq-oauth" for both the static and shared libraries. 
The
    ++# staticlib doesn't need version information in its name.
     +override shlib := lib$(NAME)$(DLSUFFIX)
    -+override stlib := lib$(NAME).a
    ++override stlib := libpq-oauth.a
     +
     +override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port 
$(LIBCURL_CPPFLAGS) $(CPPFLAGS)
     +
     +OBJS = \
    -+  $(WIN32RES) \
    -+  oauth-curl.o
    ++  $(WIN32RES)
    ++
    ++OBJS_STATIC = oauth-curl.o
     +
     +# The shared library needs additional glue symbols.
    -+$(shlib): OBJS += oauth-utils.o
    -+$(shlib): oauth-utils.o
    ++OBJS_SHLIB = \
    ++  oauth-curl_shlib.o \
    ++  oauth-utils.o \
    ++
    ++oauth-utils.o: override CPPFLAGS += -DUSE_DYNAMIC_OAUTH
    ++oauth-curl_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH
    ++
    ++# Add shlib-/stlib-specific objects.
    ++$(shlib): override OBJS += $(OBJS_SHLIB)
    ++$(shlib): $(OBJS_SHLIB)
    ++
    ++$(stlib): override OBJS += $(OBJS_STATIC)
    ++$(stlib): $(OBJS_STATIC)
     +
     +SHLIB_LINK_INTERNAL = $(libpq_pgport_shlib)
     +SHLIB_LINK = $(LIBCURL_LDFLAGS) $(LIBCURL_LDLIBS)
    @@ src/interfaces/libpq-oauth/Makefile (new)
     +# Shared library stuff
     +include $(top_srcdir)/src/Makefile.shlib
     +
    ++# Use src/common/Makefile's trick for tracking dependencies of 
shlib-specific
    ++# objects.
    ++%_shlib.o: %.c %.o
    ++  $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@
    ++
     +# Ignore the standard rules for SONAME-less installation; we want both the
     +# static and shared libraries to go into libdir.
     +install: all installdirs $(stlib) $(shlib)
    @@ src/interfaces/libpq-oauth/Makefile (new)
     +  rm -f '$(DESTDIR)$(libdir)/$(shlib)'
     +
     +clean distclean: clean-lib
    -+  rm -f $(OBJS) oauth-utils.o
    ++  rm -f $(OBJS) $(OBJS_STATIC) $(OBJS_SHLIB)
     
      ## src/interfaces/libpq-oauth/README (new) ##
     @@
    @@ src/interfaces/libpq-oauth/README (new)
     += Load-Time ABI =
     +
     +This module ABI is an internal implementation detail, so it's subject to 
change
    -+across major releases; the name of the module (libpq-oauth-MAJOR) 
reflects this.
    ++across releases; the name of the module (libpq-oauth-MAJOR-MINOR) 
reflects this.
     +The module exports the following symbols:
     +
     +- PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn);
    @@ src/interfaces/libpq-oauth/README (new)
     +
     +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.
    ++function before the flow is run.
     +
    -+This dependency injection is done to ensure that the module ABI is 
decoupled
    -+from the internals of `struct pg_conn`. 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 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.
     +
     += 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
    ++flow is enabled, the application is expected to link against libpq-oauth.a
     +directly to provide the necessary symbols.
     
      ## src/interfaces/libpq-oauth/exports.txt (new) ##
    @@ src/interfaces/libpq-oauth/meson.build (new)
     +libpq_oauth_so_sources = files(
     +  'oauth-utils.c',
     +)
    ++libpq_oauth_so_c_args = ['-DUSE_DYNAMIC_OAUTH']
     +
     +export_file = custom_target('libpq-oauth.exports',
     +  kwargs: gen_export_kwargs,
    @@ src/interfaces/libpq-oauth/meson.build (new)
     +# port needs to be in include path due to pthread-win32.h
     +libpq_oauth_inc = include_directories('.', '../libpq', '../../port')
     +
    -+# 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@'.format(pg_version_major)
    -+
    -+libpq_oauth_st = static_library(libpq_oauth_name,
    ++libpq_oauth_st = static_library('libpq-oauth',
     +  libpq_oauth_sources,
     +  include_directories: [libpq_oauth_inc, postgres_inc],
     +  c_pch: pch_postgres_fe_h,
    @@ src/interfaces/libpq-oauth/meson.build (new)
     +  kwargs: default_lib_args,
     +)
     +
    ++# 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_so = shared_module(libpq_oauth_name,
     +  libpq_oauth_sources + libpq_oauth_so_sources,
     +  include_directories: [libpq_oauth_inc, postgres_inc],
    ++  c_args: libpq_so_c_args,
     +  c_pch: pch_postgres_fe_h,
     +  dependencies: [frontend_shlib_code, libpq, libpq_oauth_deps],
     +  link_depends: export_file,
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c => 
src/interfaces/libpq-oauth/oauth-cu
     -#include "libpq-int.h"
      #include "mb/pg_wchar.h"
     +#include "oauth-curl.h"
    ++#ifdef USE_DYNAMIC_OAUTH
     +#include "oauth-utils.h"
    ++#endif
      
      /*
       * It's generally prudent to set a maximum response size to buffer in 
memory,
    @@ 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)
    + {
    +   fe_oauth_state *state = conn->sasl_state;
    +   struct async_ctx *actx;
    ++  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)
    + 
    + 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
    ++  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.
    +    */
    +   if (actx->errctx)
    +   {
    +-          appendPQExpBufferStr(&conn->errorMessage,
    +-                                                   
libpq_gettext(actx->errctx));
    +-          appendPQExpBufferStr(&conn->errorMessage, ": ");
    ++          appendPQExpBufferStr(errbuf, libpq_gettext(actx->errctx));
    ++          appendPQExpBufferStr(errbuf, ": ");
    +   }
    + 
    +   if (PQExpBufferDataBroken(actx->errbuf))
    +-          appendPQExpBufferStr(&conn->errorMessage,
    +-                                                   libpq_gettext("out of 
memory"));
    ++          appendPQExpBufferStr(errbuf, libpq_gettext("out of memory"));
    +   else
    +-          appendPQExpBufferStr(&conn->errorMessage, actx->errbuf.data);
    ++          appendPQExpBufferStr(errbuf, actx->errbuf.data);
    + 
    +   if (actx->curl_err[0])
    +   {
    +           size_t          len;
    + 
    +-          appendPQExpBuffer(&conn->errorMessage,
    +-                                            " (libcurl: %s)", 
actx->curl_err);
    ++          appendPQExpBuffer(errbuf, " (libcurl: %s)", actx->curl_err);
    + 
    +           /* Sometimes libcurl adds a newline to the error buffer. :( */
    +-          len = conn->errorMessage.len;
    +-          if (len >= 2 && conn->errorMessage.data[len - 2] == '\n')
    ++          len = errbuf->len;
    ++          if (len >= 2 && errbuf->data[len - 2] == '\n')
    +           {
    +-                  conn->errorMessage.data[len - 2] = ')';
    +-                  conn->errorMessage.data[len - 1] = '\0';
    +-                  conn->errorMessage.len--;
    ++                  errbuf->data[len - 2] = ')';
    ++                  errbuf->data[len - 1] = '\0';
    ++                  errbuf->len--;
    +           }
    +   }
    + 
    +-  appendPQExpBufferChar(&conn->errorMessage, '\n');
    ++  appendPQExpBufferChar(errbuf, '\n');
    + 
    +   return PGRES_POLLING_FAILED;
    + }
     
      ## src/interfaces/libpq-oauth/oauth-curl.h (new) ##
     @@
    @@ src/interfaces/libpq-oauth/oauth-utils.c (new)
     +#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;
    -+static conn_errorMessage_func conn_errorMessage;
     +
     +pgthreadlock_t pg_g_threadlock;
    ++conn_errorMessage_func conn_errorMessage;
     +
     +/*-
     + * Initializes libpq-oauth by setting necessary callbacks.
    @@ src/interfaces/libpq-oauth/oauth-utils.h (new)
     +                                                                          
 libpq_gettext_func gettext_impl,
     +                                                                          
 conn_errorMessage_func errmsg_impl);
     +
    ++/* Callback to safely obtain conn->errorMessage from a PGconn. */
    ++extern conn_errorMessage_func conn_errorMessage;
    ++
     +/* 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);
    @@ src/interfaces/libpq/Makefile: ifeq ($(with_ssl),openssl)
     +# libpq.so doesn't link against libcurl, but libpq.a needs libpq-oauth, 
and
     +# libpq-oauth needs libcurl. Put both into *.private.
     +PKG_CONFIG_REQUIRES_PRIVATE += libcurl
    -+%.pc: override SHLIB_LINK_INTERNAL += -lpq-oauth-$(MAJORVERSION)
    ++%.pc: override SHLIB_LINK_INTERNAL += -lpq-oauth
     +endif
     +
      all: all-lib libpq-refs-stamp
    @@ 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 DLSUFFIX;
    ++          LIBDIR "/libpq-oauth-" PG_MAJORVERSION "-" PG_MINORVERSION 
DLSUFFIX;
     +#else
    -+          "libpq-oauth-" PG_MAJORVERSION DLSUFFIX;
    ++          "libpq-oauth-" PG_MAJORVERSION "-" PG_MINORVERSION DLSUFFIX;
     +#endif
     +
     +  state->builtin_flow = dlopen(module_name, RTLD_NOW | RTLD_LOCAL);
    @@ src/interfaces/libpq/fe-auth-oauth.c: setup_token_request(PGconn *conn, 
fe_oauth
     -
     -#else
     -          libpq_append_conn_error(conn, "no custom OAuth flows are 
available, and libpq was not built with libcurl support");
    -+          libpq_append_conn_error(conn, "no custom OAuth flows are 
available, and the builtin flow is not installed");
    ++          libpq_append_conn_error(conn, "no OAuth flows are available 
(try installing the libpq-oauth package)");
                goto fail;
     -
     -#endif
    @@ src/interfaces/libpq/meson.build: libpq = declare_dependency(
     +  # libpq-oauth needs libcurl. Put both into *.private.
     +  private_deps += [
     +    libpq_oauth_deps,
    -+    '-lpq-oauth-@0@'.format(pg_version_major),
    ++    '-lpq-oauth',
     +  ]
     +endif
     +
    @@ src/test/modules/oauth_validator/t/002_client.pl: if ($ENV{with_libcurl} 
ne 'yes
                flags => ["--no-hook"],
                expected_stderr =>
     -            qr/no custom OAuth flows are available, and libpq was not 
built with libcurl support/
    -+            qr/no custom OAuth flows are available, and the builtin flow 
is not installed/
    ++            qr/no OAuth flows are available \(try installing the 
libpq-oauth package\)/
        );
      }
      
From 5f87f11b18ea83615c342c832caace49bf7e3897 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Mon, 21 Apr 2025 13:43:08 -0700
Subject: [PATCH v8 1/2] Add minor-version counterpart to (PG_)MAJORVERSION

An upcoming commit will name a library, libpq-oauth, using the major and
minor versions. Make the minor version accessible from the Makefiles and
as a string constant in the code.
---
 configure                  | 7 +++++++
 configure.ac               | 2 ++
 meson.build                | 1 +
 src/Makefile.global.in     | 1 +
 src/include/pg_config.h.in | 3 +++
 src/makefiles/meson.build  | 1 +
 6 files changed, 15 insertions(+)

diff --git a/configure b/configure
index 0936010718d..3d783793dfa 100755
--- a/configure
+++ b/configure
@@ -792,6 +792,7 @@ build_os
 build_vendor
 build_cpu
 build
+PG_MINORVERSION
 PG_MAJORVERSION
 target_alias
 host_alias
@@ -2877,6 +2878,12 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
+
+cat >>confdefs.h <<_ACEOF
+#define PG_MINORVERSION "$PG_MINORVERSION"
+_ACEOF
+
+
 cat >>confdefs.h <<_ACEOF
 #define PG_MINORVERSION_NUM $PG_MINORVERSION
 _ACEOF
diff --git a/configure.ac b/configure.ac
index 2a78cddd825..1cb3a0ff042 100644
--- a/configure.ac
+++ b/configure.ac
@@ -35,6 +35,8 @@ test -n "$PG_MINORVERSION" || PG_MINORVERSION=0
 AC_SUBST(PG_MAJORVERSION)
 AC_DEFINE_UNQUOTED(PG_MAJORVERSION, "$PG_MAJORVERSION", [PostgreSQL major version as a string])
 AC_DEFINE_UNQUOTED(PG_MAJORVERSION_NUM, $PG_MAJORVERSION, [PostgreSQL major version number])
+AC_SUBST(PG_MINORVERSION)
+AC_DEFINE_UNQUOTED(PG_MINORVERSION, "$PG_MINORVERSION", [PostgreSQL minor version as a string])
 AC_DEFINE_UNQUOTED(PG_MINORVERSION_NUM, $PG_MINORVERSION, [PostgreSQL minor version number])
 
 PGAC_ARG_REQ(with, extra-version, [STRING], [append STRING to version],
diff --git a/meson.build b/meson.build
index a1516e54529..18423a7c13e 100644
--- a/meson.build
+++ b/meson.build
@@ -148,6 +148,7 @@ pg_version += get_option('extra_version')
 cdata.set_quoted('PG_VERSION', pg_version)
 cdata.set_quoted('PG_MAJORVERSION', pg_version_major.to_string())
 cdata.set('PG_MAJORVERSION_NUM', pg_version_major)
+cdata.set_quoted('PG_MINORVERSION', pg_version_minor.to_string())
 cdata.set('PG_MINORVERSION_NUM', pg_version_minor)
 cdata.set('PG_VERSION_NUM', pg_version_num)
 # PG_VERSION_STR is built later, it depends on compiler test results
diff --git a/src/Makefile.global.in b/src/Makefile.global.in
index 6722fbdf365..54b4a07712e 100644
--- a/src/Makefile.global.in
+++ b/src/Makefile.global.in
@@ -40,6 +40,7 @@ maintainer-clean: distclean
 # PostgreSQL version number
 VERSION = @PACKAGE_VERSION@
 MAJORVERSION = @PG_MAJORVERSION@
+MINORVERSION = @PG_MINORVERSION@
 VERSION_NUM = @PG_VERSION_NUM@
 
 PACKAGE_URL = @PACKAGE_URL@
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index c3cc9fa856d..4fe37d228c5 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -602,6 +602,9 @@
 /* PostgreSQL major version number */
 #undef PG_MAJORVERSION_NUM
 
+/* PostgreSQL minor version as a string */
+#undef PG_MINORVERSION
+
 /* PostgreSQL minor version number */
 #undef PG_MINORVERSION_NUM
 
diff --git a/src/makefiles/meson.build b/src/makefiles/meson.build
index 55da678ec27..e3adb5d8dc4 100644
--- a/src/makefiles/meson.build
+++ b/src/makefiles/meson.build
@@ -36,6 +36,7 @@ pgxs_kv = {
   'PACKAGE_URL': pg_url,
   'PACKAGE_VERSION': pg_version,
   'PG_MAJORVERSION': pg_version_major,
+  'PG_MINORVERSION': pg_version_minor,
   'PG_VERSION_NUM': pg_version_num,
   'configure_input': 'meson',
 
-- 
2.34.1

From 4c9cc7f69afd8f59f98b38900e53fc199d4b4009 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Wed, 26 Mar 2025 10:55:28 -0700
Subject: [PATCH v8 2/2] oauth: Move the builtin flow into a separate module

The additional packaging footprint of the OAuth Curl dependency, as well
as the existence of libcurl in the address space even if OAuth isn't
ever used by a client, has raised some concerns. Split off this
dependency into a separate loadable module called libpq-oauth.

When configured using --with-libcurl, libpq.so searches for this new
module via dlopen(). End users may choose not to install the libpq-oauth
module, in which case the default flow is disabled.

For static applications using libpq.a, the libpq-oauth staticlib is a
mandatory link-time dependency for --with-libcurl builds. libpq.pc has
been updated accordingly.

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.

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.)

Additionally, NLS support for error messages in b3f0be788a was
incomplete, because the new error macros weren't being scanned by
xgettext. Fix that now.

Per request from Tom Lane and Bruce Momjian. Based on an initial patch
by Daniel Gustafsson, who also contributed docs changes. The "bare"
dlopen() concept came from Thomas Munro. Many many people reviewed the
design and implementation; thank you!

Co-authored-by: Daniel Gustafsson <daniel@yesql.se>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Christoph Berg <myon@debian.org>
Reviewed-by: Jelte Fennema-Nio <postgres@jeltef.nl>
Reviewed-by: Peter Eisentraut <peter@eisentraut.org>
Reviewed-by: Wolfgang Walther <walther@technowledgy.de>
Discussion: https://postgr.es/m/641687.1742360249%40sss.pgh.pa.us
---
 config/programs.m4                            |  17 +-
 configure                                     |  50 ++++-
 configure.ac                                  |  26 ++-
 doc/src/sgml/installation.sgml                |   8 +
 doc/src/sgml/libpq.sgml                       |  30 ++-
 meson.build                                   |  32 ++-
 src/Makefile.global.in                        |   3 +
 src/interfaces/Makefile                       |  12 ++
 src/interfaces/libpq-oauth/Makefile           |  83 +++++++
 src/interfaces/libpq-oauth/README             |  43 ++++
 src/interfaces/libpq-oauth/exports.txt        |   4 +
 src/interfaces/libpq-oauth/meson.build        |  45 ++++
 .../oauth-curl.c}                             |  99 +++++----
 src/interfaces/libpq-oauth/oauth-curl.h       |  24 +++
 src/interfaces/libpq-oauth/oauth-utils.c      | 202 ++++++++++++++++++
 src/interfaces/libpq-oauth/oauth-utils.h      |  38 ++++
 src/interfaces/libpq/Makefile                 |  36 +++-
 src/interfaces/libpq/exports.txt              |   1 +
 src/interfaces/libpq/fe-auth-oauth.c          | 197 ++++++++++++++++-
 src/interfaces/libpq/fe-auth-oauth.h          |   5 +-
 src/interfaces/libpq/meson.build              |  25 ++-
 src/interfaces/libpq/nls.mk                   |  12 +-
 src/makefiles/meson.build                     |   2 +
 src/test/modules/oauth_validator/meson.build  |   2 +-
 .../modules/oauth_validator/t/002_client.pl   |   2 +-
 25 files changed, 886 insertions(+), 112 deletions(-)
 create mode 100644 src/interfaces/libpq-oauth/Makefile
 create mode 100644 src/interfaces/libpq-oauth/README
 create mode 100644 src/interfaces/libpq-oauth/exports.txt
 create mode 100644 src/interfaces/libpq-oauth/meson.build
 rename src/interfaces/{libpq/fe-auth-oauth-curl.c => libpq-oauth/oauth-curl.c} (97%)
 create mode 100644 src/interfaces/libpq-oauth/oauth-curl.h
 create mode 100644 src/interfaces/libpq-oauth/oauth-utils.c
 create mode 100644 src/interfaces/libpq-oauth/oauth-utils.h

diff --git a/config/programs.m4 b/config/programs.m4
index 0a07feb37cc..0ad1e58b48d 100644
--- a/config/programs.m4
+++ b/config/programs.m4
@@ -286,9 +286,20 @@ AC_DEFUN([PGAC_CHECK_LIBCURL],
 [
   AC_CHECK_HEADER(curl/curl.h, [],
 				  [AC_MSG_ERROR([header file <curl/curl.h> is required for --with-libcurl])])
-  AC_CHECK_LIB(curl, curl_multi_init, [],
+  AC_CHECK_LIB(curl, curl_multi_init, [
+				 AC_DEFINE([HAVE_LIBCURL], [1], [Define to 1 if you have the `curl' library (-lcurl).])
+				 AC_SUBST(LIBCURL_LDLIBS, -lcurl)
+			   ],
 			   [AC_MSG_ERROR([library 'curl' does not provide curl_multi_init])])
 
+  pgac_save_CPPFLAGS=$CPPFLAGS
+  pgac_save_LDFLAGS=$LDFLAGS
+  pgac_save_LIBS=$LIBS
+
+  CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS"
+  LDFLAGS="$LIBCURL_LDFLAGS $LDFLAGS"
+  LIBS="$LIBCURL_LDLIBS $LIBS"
+
   # Check to see whether the current platform supports threadsafe Curl
   # initialization.
   AC_CACHE_CHECK([for curl_global_init thread safety], [pgac_cv__libcurl_threadsafe_init],
@@ -338,4 +349,8 @@ AC_DEFUN([PGAC_CHECK_LIBCURL],
 *** lookups. Rebuild libcurl with the AsynchDNS feature enabled in order
 *** to use it with libpq.])
   fi
+
+  CPPFLAGS=$pgac_save_CPPFLAGS
+  LDFLAGS=$pgac_save_LDFLAGS
+  LIBS=$pgac_save_LIBS
 ])# PGAC_CHECK_LIBCURL
diff --git a/configure b/configure
index 3d783793dfa..eedd18e6d9a 100755
--- a/configure
+++ b/configure
@@ -655,6 +655,7 @@ UUID_LIBS
 LDAP_LIBS_BE
 LDAP_LIBS_FE
 with_ssl
+LIBCURL_LDLIBS
 PTHREAD_CFLAGS
 PTHREAD_LIBS
 PTHREAD_CC
@@ -711,6 +712,8 @@ with_libxml
 LIBNUMA_LIBS
 LIBNUMA_CFLAGS
 with_libnuma
+LIBCURL_LDFLAGS
+LIBCURL_CPPFLAGS
 LIBCURL_LIBS
 LIBCURL_CFLAGS
 with_libcurl
@@ -9060,19 +9063,27 @@ $as_echo "yes" >&6; }
 
 fi
 
-  # We only care about -I, -D, and -L switches;
-  # note that -lcurl will be added by PGAC_CHECK_LIBCURL below.
+  # Curl's flags are kept separate from the standard CPPFLAGS/LDFLAGS. We use
+  # them only for libpq-oauth.
+  LIBCURL_CPPFLAGS=
+  LIBCURL_LDFLAGS=
+
+  # We only care about -I, -D, and -L switches. Note that -lcurl will be added
+  # to LIBCURL_LDLIBS by PGAC_CHECK_LIBCURL, below.
   for pgac_option in $LIBCURL_CFLAGS; do
     case $pgac_option in
-      -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";;
+      -I*|-D*) LIBCURL_CPPFLAGS="$LIBCURL_CPPFLAGS $pgac_option";;
     esac
   done
   for pgac_option in $LIBCURL_LIBS; do
     case $pgac_option in
-      -L*) LDFLAGS="$LDFLAGS $pgac_option";;
+      -L*) LIBCURL_LDFLAGS="$LIBCURL_LDFLAGS $pgac_option";;
     esac
   done
 
+
+
+
   # OAuth requires python for testing
   if test "$with_python" != yes; then
     { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: *** OAuth support tests require --with-python to run" >&5
@@ -12711,9 +12722,6 @@ fi
 
 fi
 
-# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults
-# during gss_acquire_cred(). This is possibly related to Curl's Heimdal
-# dependency on that platform?
 if test "$with_libcurl" = yes ; then
 
   ac_fn_c_check_header_mongrel "$LINENO" "curl/curl.h" "ac_cv_header_curl_curl_h" "$ac_includes_default"
@@ -12761,17 +12769,26 @@ fi
 { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curl_curl_multi_init" >&5
 $as_echo "$ac_cv_lib_curl_curl_multi_init" >&6; }
 if test "x$ac_cv_lib_curl_curl_multi_init" = xyes; then :
-  cat >>confdefs.h <<_ACEOF
-#define HAVE_LIBCURL 1
-_ACEOF
 
-  LIBS="-lcurl $LIBS"
+
+$as_echo "#define HAVE_LIBCURL 1" >>confdefs.h
+
+				 LIBCURL_LDLIBS=-lcurl
+
 
 else
   as_fn_error $? "library 'curl' does not provide curl_multi_init" "$LINENO" 5
 fi
 
 
+  pgac_save_CPPFLAGS=$CPPFLAGS
+  pgac_save_LDFLAGS=$LDFLAGS
+  pgac_save_LIBS=$LIBS
+
+  CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS"
+  LDFLAGS="$LIBCURL_LDFLAGS $LDFLAGS"
+  LIBS="$LIBCURL_LDLIBS $LIBS"
+
   # Check to see whether the current platform supports threadsafe Curl
   # initialization.
   { $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl_global_init thread safety" >&5
@@ -12875,6 +12892,10 @@ $as_echo "$pgac_cv__libcurl_async_dns" >&6; }
 *** to use it with libpq." "$LINENO" 5
   fi
 
+  CPPFLAGS=$pgac_save_CPPFLAGS
+  LDFLAGS=$pgac_save_LDFLAGS
+  LIBS=$pgac_save_LIBS
+
 fi
 
 if test "$with_gssapi" = yes ; then
@@ -14523,6 +14544,13 @@ done
 
 fi
 
+if test "$with_libcurl" = yes ; then
+  # Error out early if this platform can't support libpq-oauth.
+  if test "$ac_cv_header_sys_event_h" != yes -a "$ac_cv_header_sys_epoll_h" != yes; then
+    as_fn_error $? "client OAuth is not supported on this platform" "$LINENO" 5
+  fi
+fi
+
 ##
 ## Types, structures, compiler characteristics
 ##
diff --git a/configure.ac b/configure.ac
index 1cb3a0ff042..7329b23d309 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1035,19 +1035,27 @@ if test "$with_libcurl" = yes ; then
   # to explicitly set TLS 1.3 ciphersuites).
   PKG_CHECK_MODULES(LIBCURL, [libcurl >= 7.61.0])
 
-  # We only care about -I, -D, and -L switches;
-  # note that -lcurl will be added by PGAC_CHECK_LIBCURL below.
+  # Curl's flags are kept separate from the standard CPPFLAGS/LDFLAGS. We use
+  # them only for libpq-oauth.
+  LIBCURL_CPPFLAGS=
+  LIBCURL_LDFLAGS=
+
+  # We only care about -I, -D, and -L switches. Note that -lcurl will be added
+  # to LIBCURL_LDLIBS by PGAC_CHECK_LIBCURL, below.
   for pgac_option in $LIBCURL_CFLAGS; do
     case $pgac_option in
-      -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";;
+      -I*|-D*) LIBCURL_CPPFLAGS="$LIBCURL_CPPFLAGS $pgac_option";;
     esac
   done
   for pgac_option in $LIBCURL_LIBS; do
     case $pgac_option in
-      -L*) LDFLAGS="$LDFLAGS $pgac_option";;
+      -L*) LIBCURL_LDFLAGS="$LIBCURL_LDFLAGS $pgac_option";;
     esac
   done
 
+  AC_SUBST(LIBCURL_CPPFLAGS)
+  AC_SUBST(LIBCURL_LDFLAGS)
+
   # OAuth requires python for testing
   if test "$with_python" != yes; then
     AC_MSG_WARN([*** OAuth support tests require --with-python to run])
@@ -1356,9 +1364,6 @@ failure.  It is possible the compiler isn't looking in the proper directory.
 Use --without-zlib to disable zlib support.])])
 fi
 
-# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults
-# during gss_acquire_cred(). This is possibly related to Curl's Heimdal
-# dependency on that platform?
 if test "$with_libcurl" = yes ; then
   PGAC_CHECK_LIBCURL
 fi
@@ -1656,6 +1661,13 @@ if test "$PORTNAME" = "win32" ; then
    AC_CHECK_HEADERS(crtdefs.h)
 fi
 
+if test "$with_libcurl" = yes ; then
+  # Error out early if this platform can't support libpq-oauth.
+  if test "$ac_cv_header_sys_event_h" != yes -a "$ac_cv_header_sys_epoll_h" != yes; then
+    AC_MSG_ERROR([client OAuth is not supported on this platform])
+  fi
+fi
+
 ##
 ## Types, structures, compiler characteristics
 ##
diff --git a/doc/src/sgml/installation.sgml b/doc/src/sgml/installation.sgml
index 077bcc20759..d928b103d22 100644
--- a/doc/src/sgml/installation.sgml
+++ b/doc/src/sgml/installation.sgml
@@ -313,6 +313,14 @@
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      You need <productname>Curl</productname> to build an optional module
+      which implements the <link linkend="libpq-oauth">OAuth Device
+      Authorization flow</link> for client applications.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       You need <productname>LZ4</productname>, if you want to support
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 3be66789ba7..cd748902f4d 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -10226,15 +10226,20 @@ void PQinitSSL(int do_ssl);
   <title>OAuth Support</title>
 
   <para>
-   libpq implements support for the OAuth v2 Device Authorization client flow,
+   <application>libpq</application> implements support for the OAuth v2 Device Authorization client flow,
    documented in
    <ulink url="https://datatracker.ietf.org/doc/html/rfc8628";>RFC 8628</ulink>,
-   which it will attempt to use by default if the server
+   as an optional module. See the <link linkend="configure-option-with-libcurl">
+   installation documentation</link> for information on how to enable support
+   for Device Authorization as a builtin flow.
+  </para>
+  <para>
+   When support is enabled and the optional module installed, <application>libpq</application>
+   will use the builtin flow by default if the server
    <link linkend="auth-oauth">requests a bearer token</link> during
    authentication. This flow can be utilized even if the system running the
    client application does not have a usable web browser, for example when
-   running a client via <application>SSH</application>. Client applications may implement their own flows
-   instead; see <xref linkend="libpq-oauth-authdata-hooks"/>.
+   running a client via <acronym>SSH</acronym>.
   </para>
   <para>
    The builtin flow will, by default, print a URL to visit and a user code to
@@ -10251,6 +10256,11 @@ Visit https://example.com/device and enter the code: ABCD-EFGH
    they match expectations, before continuing. Permissions should not be given
    to untrusted third parties.
   </para>
+  <para>
+   Client applications may implement their own flows to customize interaction
+   and integration with applications. See <xref linkend="libpq-oauth-authdata-hooks"/>
+   for more information on how add a custom flow to <application>libpq</application>.
+  </para>
   <para>
    For an OAuth client flow to be usable, the connection string must at minimum
    contain <xref linkend="libpq-connect-oauth-issuer"/> and
@@ -10366,7 +10376,9 @@ typedef struct _PGpromptOAuthDevice
 </synopsis>
         </para>
         <para>
-         The OAuth Device Authorization flow included in <application>libpq</application>
+         The OAuth Device Authorization flow which
+         <link linkend="configure-option-with-libcurl">can be included</link>
+         in <application>libpq</application>
          requires the end user to visit a URL with a browser, then enter a code
          which permits <application>libpq</application> to connect to the server
          on their behalf. The default prompt simply prints the
@@ -10378,7 +10390,8 @@ typedef struct _PGpromptOAuthDevice
          This callback is only invoked during the builtin device
          authorization flow. If the application installs a
          <link linkend="libpq-oauth-authdata-oauth-bearer-token">custom OAuth
-         flow</link>, this authdata type will not be used.
+         flow</link>, or <application>libpq</application> was not built with
+         support for the builtin flow, this authdata type will not be used.
         </para>
         <para>
          If a non-NULL <structfield>verification_uri_complete</structfield> is
@@ -10400,8 +10413,9 @@ typedef struct _PGpromptOAuthDevice
        </term>
        <listitem>
         <para>
-         Replaces the entire OAuth flow with a custom implementation. The hook
-         should either directly return a Bearer token for the current
+         Adds a custom implementation of a flow, replacing the builtin flow if
+         it is <link linkend="configure-option-with-libcurl">installed</link>.
+         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.
         </para>
diff --git a/meson.build b/meson.build
index 18423a7c13e..6787683ca27 100644
--- a/meson.build
+++ b/meson.build
@@ -107,6 +107,7 @@ os_deps = []
 backend_both_deps = []
 backend_deps = []
 libpq_deps = []
+libpq_oauth_deps = []
 
 pg_sysroot = ''
 
@@ -861,13 +862,13 @@ endif
 ###############################################################
 
 libcurlopt = get_option('libcurl')
+oauth_flow_supported = false
+
 if not libcurlopt.disabled()
   # Check for libcurl 7.61.0 or higher (corresponding to RHEL8 and the ability
   # to explicitly set TLS 1.3 ciphersuites).
   libcurl = dependency('libcurl', version: '>= 7.61.0', required: libcurlopt)
   if libcurl.found()
-    cdata.set('USE_LIBCURL', 1)
-
     # Check to see whether the current platform supports thread-safe Curl
     # initialization.
     libcurl_threadsafe_init = false
@@ -939,6 +940,22 @@ if not libcurlopt.disabled()
     endif
   endif
 
+  # Check that the current platform supports our builtin flow. This requires
+  # libcurl and one of either epoll or kqueue.
+  oauth_flow_supported = (
+    libcurl.found()
+    and (cc.check_header('sys/event.h', required: false,
+                         args: test_c_args, include_directories: postgres_inc)
+         or cc.check_header('sys/epoll.h', required: false,
+                            args: test_c_args, include_directories: postgres_inc))
+  )
+
+  if oauth_flow_supported
+    cdata.set('USE_LIBCURL', 1)
+  elif libcurlopt.enabled()
+    error('client OAuth is not supported on this platform')
+  endif
+
 else
   libcurl = not_found_dep
 endif
@@ -3273,17 +3290,18 @@ libpq_deps += [
 
   gssapi,
   ldap_r,
-  # XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults
-  # during gss_acquire_cred(). This is possibly related to Curl's Heimdal
-  # dependency on that platform?
-  libcurl,
   libintl,
   ssl,
 ]
 
+libpq_oauth_deps += [
+  libcurl,
+]
+
 subdir('src/interfaces/libpq')
-# fe_utils depends on libpq
+# fe_utils and libpq-oauth depends on libpq
 subdir('src/fe_utils')
+subdir('src/interfaces/libpq-oauth')
 
 # for frontend binaries
 frontend_code = declare_dependency(
diff --git a/src/Makefile.global.in b/src/Makefile.global.in
index 54b4a07712e..f4caece04df 100644
--- a/src/Makefile.global.in
+++ b/src/Makefile.global.in
@@ -348,6 +348,9 @@ perl_embed_ldflags	= @perl_embed_ldflags@
 
 AWK	= @AWK@
 LN_S	= @LN_S@
+LIBCURL_CPPFLAGS = @LIBCURL_CPPFLAGS@
+LIBCURL_LDFLAGS = @LIBCURL_LDFLAGS@
+LIBCURL_LDLIBS = @LIBCURL_LDLIBS@
 MSGFMT  = @MSGFMT@
 MSGFMT_FLAGS = @MSGFMT_FLAGS@
 MSGMERGE = @MSGMERGE@
diff --git a/src/interfaces/Makefile b/src/interfaces/Makefile
index 7d56b29d28f..e6822caa206 100644
--- a/src/interfaces/Makefile
+++ b/src/interfaces/Makefile
@@ -14,7 +14,19 @@ include $(top_builddir)/src/Makefile.global
 
 SUBDIRS = libpq ecpg
 
+ifeq ($(with_libcurl), yes)
+SUBDIRS += libpq-oauth
+else
+ALWAYS_SUBDIRS += libpq-oauth
+endif
+
 $(recurse)
+$(recurse_always)
 
 all-ecpg-recurse: all-libpq-recurse
 install-ecpg-recurse: install-libpq-recurse
+
+ifeq ($(with_libcurl), yes)
+all-libpq-oauth-recurse: all-libpq-recurse
+install-libpq-oauth-recurse: install-libpq-recurse
+endif
diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile
new file mode 100644
index 00000000000..98acaff1a3b
--- /dev/null
+++ b/src/interfaces/libpq-oauth/Makefile
@@ -0,0 +1,83 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for libpq-oauth
+#
+# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/interfaces/libpq-oauth/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/interfaces/libpq-oauth
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+PGFILEDESC = "libpq-oauth - device authorization OAuth support"
+
+# This is an internal module; we don't want an SONAME and therefore do not set
+# SO_MAJOR_VERSION.
+NAME = pq-oauth-$(MAJORVERSION)-$(MINORVERSION)
+
+# Force the name "libpq-oauth" for both the static and shared libraries. The
+# staticlib doesn't need version information in its name.
+override shlib := lib$(NAME)$(DLSUFFIX)
+override stlib := libpq-oauth.a
+
+override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port $(LIBCURL_CPPFLAGS) $(CPPFLAGS)
+
+OBJS = \
+	$(WIN32RES)
+
+OBJS_STATIC = oauth-curl.o
+
+# The shared library needs additional glue symbols.
+OBJS_SHLIB = \
+	oauth-curl_shlib.o \
+	oauth-utils.o \
+
+oauth-utils.o: override CPPFLAGS += -DUSE_DYNAMIC_OAUTH
+oauth-curl_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH
+
+# Add shlib-/stlib-specific objects.
+$(shlib): override OBJS += $(OBJS_SHLIB)
+$(shlib): $(OBJS_SHLIB)
+
+$(stlib): override OBJS += $(OBJS_STATIC)
+$(stlib): $(OBJS_STATIC)
+
+SHLIB_LINK_INTERNAL = $(libpq_pgport_shlib)
+SHLIB_LINK = $(LIBCURL_LDFLAGS) $(LIBCURL_LDLIBS)
+SHLIB_PREREQS = submake-libpq
+SHLIB_EXPORTS = exports.txt
+
+# Disable -bundle_loader on macOS.
+BE_DLLLIBS =
+
+# By default, a library without an SONAME doesn't get a static library, so we
+# add it to the build explicitly.
+all: all-lib all-static-lib
+
+# Shared library stuff
+include $(top_srcdir)/src/Makefile.shlib
+
+# Use src/common/Makefile's trick for tracking dependencies of shlib-specific
+# objects.
+%_shlib.o: %.c %.o
+	$(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@
+
+# Ignore the standard rules for SONAME-less installation; we want both the
+# static and shared libraries to go into libdir.
+install: all installdirs $(stlib) $(shlib)
+	$(INSTALL_SHLIB) $(shlib) '$(DESTDIR)$(libdir)/$(shlib)'
+	$(INSTALL_STLIB) $(stlib) '$(DESTDIR)$(libdir)/$(stlib)'
+
+installdirs:
+	$(MKDIR_P) '$(DESTDIR)$(libdir)'
+
+uninstall:
+	rm -f '$(DESTDIR)$(libdir)/$(stlib)'
+	rm -f '$(DESTDIR)$(libdir)/$(shlib)'
+
+clean distclean: clean-lib
+	rm -f $(OBJS) $(OBJS_STATIC) $(OBJS_SHLIB)
diff --git a/src/interfaces/libpq-oauth/README b/src/interfaces/libpq-oauth/README
new file mode 100644
index 00000000000..fdc1320d152
--- /dev/null
+++ b/src/interfaces/libpq-oauth/README
@@ -0,0 +1,43 @@
+libpq-oauth is an optional module implementing the Device Authorization flow for
+OAuth clients (RFC 8628). It was originally developed as part of libpq core and
+later split out as its own shared library in order to isolate its dependency on
+libcurl. (End users who don't want the Curl dependency can simply choose not to
+install this module.)
+
+If a connection string allows the use of OAuth, and the server asks for it, and
+a libpq client has not installed its own custom OAuth flow, libpq will attempt
+to delay-load this module using dlopen() and the following ABI. Failure to load
+results in a failed connection.
+
+= 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.
+The module exports the following symbols:
+
+- PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn);
+- void pg_fe_cleanup_oauth_flow(PGconn *conn);
+
+pg_fe_run_oauth_flow and pg_fe_cleanup_oauth_flow are implementations of
+conn->async_auth and conn->cleanup_async_auth, respectively.
+
+- void libpq_oauth_init(pgthreadlock_t threadlock,
+						libpq_gettext_func gettext_impl,
+						conn_errorMessage_func errmsg_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.
+
+= 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.
diff --git a/src/interfaces/libpq-oauth/exports.txt b/src/interfaces/libpq-oauth/exports.txt
new file mode 100644
index 00000000000..6891a83dbf9
--- /dev/null
+++ b/src/interfaces/libpq-oauth/exports.txt
@@ -0,0 +1,4 @@
+# src/interfaces/libpq-oauth/exports.txt
+libpq_oauth_init          1
+pg_fe_run_oauth_flow      2
+pg_fe_cleanup_oauth_flow  3
diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build
new file mode 100644
index 00000000000..d97f893178a
--- /dev/null
+++ b/src/interfaces/libpq-oauth/meson.build
@@ -0,0 +1,45 @@
+# Copyright (c) 2022-2025, PostgreSQL Global Development Group
+
+if not oauth_flow_supported
+  subdir_done()
+endif
+
+libpq_oauth_sources = files(
+  'oauth-curl.c',
+)
+
+# The shared library needs additional glue symbols.
+libpq_oauth_so_sources = files(
+  'oauth-utils.c',
+)
+libpq_oauth_so_c_args = ['-DUSE_DYNAMIC_OAUTH']
+
+export_file = custom_target('libpq-oauth.exports',
+  kwargs: gen_export_kwargs,
+)
+
+# port needs to be in include path due to pthread-win32.h
+libpq_oauth_inc = include_directories('.', '../libpq', '../../port')
+
+libpq_oauth_st = static_library('libpq-oauth',
+  libpq_oauth_sources,
+  include_directories: [libpq_oauth_inc, postgres_inc],
+  c_pch: pch_postgres_fe_h,
+  dependencies: [frontend_stlib_code, libpq_oauth_deps],
+  kwargs: default_lib_args,
+)
+
+# 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_so = shared_module(libpq_oauth_name,
+  libpq_oauth_sources + libpq_oauth_so_sources,
+  include_directories: [libpq_oauth_inc, postgres_inc],
+  c_args: libpq_so_c_args,
+  c_pch: pch_postgres_fe_h,
+  dependencies: [frontend_shlib_code, libpq, libpq_oauth_deps],
+  link_depends: export_file,
+  link_args: export_fmt.format(export_file.full_path()),
+  kwargs: default_lib_args,
+)
diff --git a/src/interfaces/libpq/fe-auth-oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
similarity index 97%
rename from src/interfaces/libpq/fe-auth-oauth-curl.c
rename to src/interfaces/libpq-oauth/oauth-curl.c
index c195e00cd28..3239315d952 100644
--- a/src/interfaces/libpq/fe-auth-oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -1,6 +1,6 @@
 /*-------------------------------------------------------------------------
  *
- * fe-auth-oauth-curl.c
+ * oauth-curl.c
  *	   The libcurl implementation of OAuth/OIDC authentication, using the
  *	   OAuth Device Authorization Grant (RFC 8628).
  *
@@ -8,7 +8,7 @@
  * Portions Copyright (c) 1994, Regents of the University of California
  *
  * IDENTIFICATION
- *	  src/interfaces/libpq/fe-auth-oauth-curl.c
+ *	  src/interfaces/libpq-oauth/oauth-curl.c
  *
  *-------------------------------------------------------------------------
  */
@@ -17,20 +17,25 @@
 
 #include <curl/curl.h>
 #include <math.h>
-#ifdef HAVE_SYS_EPOLL_H
+#include <unistd.h>
+
+#if defined(HAVE_SYS_EPOLL_H)
 #include <sys/epoll.h>
 #include <sys/timerfd.h>
-#endif
-#ifdef HAVE_SYS_EVENT_H
+#elif defined(HAVE_SYS_EVENT_H)
 #include <sys/event.h>
+#else
+#error libpq-oauth is not supported on this platform
 #endif
-#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"
+#include "oauth-curl.h"
+#ifdef USE_DYNAMIC_OAUTH
+#include "oauth-utils.h"
+#endif
 
 /*
  * It's generally prudent to set a maximum response size to buffer in memory,
@@ -1110,7 +1115,7 @@ parse_access_token(struct async_ctx *actx, struct token *tok)
 static bool
 setup_multiplexer(struct async_ctx *actx)
 {
-#ifdef HAVE_SYS_EPOLL_H
+#if defined(HAVE_SYS_EPOLL_H)
 	struct epoll_event ev = {.events = EPOLLIN};
 
 	actx->mux = epoll_create1(EPOLL_CLOEXEC);
@@ -1134,8 +1139,7 @@ setup_multiplexer(struct async_ctx *actx)
 	}
 
 	return true;
-#endif
-#ifdef HAVE_SYS_EVENT_H
+#elif defined(HAVE_SYS_EVENT_H)
 	actx->mux = kqueue();
 	if (actx->mux < 0)
 	{
@@ -1158,10 +1162,9 @@ setup_multiplexer(struct async_ctx *actx)
 	}
 
 	return true;
+#else
+#error setup_multiplexer is not implemented on this platform
 #endif
-
-	actx_error(actx, "libpq does not support the Device Authorization flow on this platform");
-	return false;
 }
 
 /*
@@ -1174,7 +1177,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
 {
 	struct async_ctx *actx = ctx;
 
-#ifdef HAVE_SYS_EPOLL_H
+#if defined(HAVE_SYS_EPOLL_H)
 	struct epoll_event ev = {0};
 	int			res;
 	int			op = EPOLL_CTL_ADD;
@@ -1230,8 +1233,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
 	}
 
 	return 0;
-#endif
-#ifdef HAVE_SYS_EVENT_H
+#elif defined(HAVE_SYS_EVENT_H)
 	struct kevent ev[2] = {0};
 	struct kevent ev_out[2];
 	struct timespec timeout = {0};
@@ -1312,10 +1314,9 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
 	}
 
 	return 0;
+#else
+#error register_socket is not implemented on this platform
 #endif
-
-	actx_error(actx, "libpq does not support multiplexer sockets on this platform");
-	return -1;
 }
 
 /*
@@ -1334,7 +1335,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
 static bool
 set_timer(struct async_ctx *actx, long timeout)
 {
-#if HAVE_SYS_EPOLL_H
+#if defined(HAVE_SYS_EPOLL_H)
 	struct itimerspec spec = {0};
 
 	if (timeout < 0)
@@ -1363,8 +1364,7 @@ set_timer(struct async_ctx *actx, long timeout)
 	}
 
 	return true;
-#endif
-#ifdef HAVE_SYS_EVENT_H
+#elif defined(HAVE_SYS_EVENT_H)
 	struct kevent ev;
 
 #ifdef __NetBSD__
@@ -1419,10 +1419,9 @@ set_timer(struct async_ctx *actx, long timeout)
 	}
 
 	return true;
+#else
+#error set_timer is not implemented on this platform
 #endif
-
-	actx_error(actx, "libpq does not support timers on this platform");
-	return false;
 }
 
 /*
@@ -1433,7 +1432,7 @@ set_timer(struct async_ctx *actx, long timeout)
 static int
 timer_expired(struct async_ctx *actx)
 {
-#if HAVE_SYS_EPOLL_H
+#if defined(HAVE_SYS_EPOLL_H)
 	struct itimerspec spec = {0};
 
 	if (timerfd_gettime(actx->timerfd, &spec) < 0)
@@ -1453,8 +1452,7 @@ timer_expired(struct async_ctx *actx)
 	/* If the remaining time to expiration is zero, we're done. */
 	return (spec.it_value.tv_sec == 0
 			&& spec.it_value.tv_nsec == 0);
-#endif
-#ifdef HAVE_SYS_EVENT_H
+#elif defined(HAVE_SYS_EVENT_H)
 	int			res;
 
 	/* Is the timer queue ready? */
@@ -1466,10 +1464,9 @@ timer_expired(struct async_ctx *actx)
 	}
 
 	return (res > 0);
+#else
+#error timer_expired is not implemented on this platform
 #endif
-
-	actx_error(actx, "libpq does not support timers on this platform");
-	return -1;
 }
 
 /*
@@ -2487,8 +2484,9 @@ prompt_user(struct async_ctx *actx, PGconn *conn)
 		.verification_uri_complete = actx->authz.verification_uri_complete,
 		.expires_in = actx->authz.expires_in,
 	};
+	PQauthDataHook_type hook = PQgetAuthDataHook();
 
-	res = PQauthDataHook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt);
+	res = hook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt);
 
 	if (!res)
 	{
@@ -2635,6 +2633,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
 {
 	fe_oauth_state *state = conn->sasl_state;
 	struct async_ctx *actx;
+	PQExpBuffer errbuf;
 
 	if (!initialize_curl(conn))
 		return PGRES_POLLING_FAILED;
@@ -2825,41 +2824,49 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
 
 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
+	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.
 	 */
 	if (actx->errctx)
 	{
-		appendPQExpBufferStr(&conn->errorMessage,
-							 libpq_gettext(actx->errctx));
-		appendPQExpBufferStr(&conn->errorMessage, ": ");
+		appendPQExpBufferStr(errbuf, libpq_gettext(actx->errctx));
+		appendPQExpBufferStr(errbuf, ": ");
 	}
 
 	if (PQExpBufferDataBroken(actx->errbuf))
-		appendPQExpBufferStr(&conn->errorMessage,
-							 libpq_gettext("out of memory"));
+		appendPQExpBufferStr(errbuf, libpq_gettext("out of memory"));
 	else
-		appendPQExpBufferStr(&conn->errorMessage, actx->errbuf.data);
+		appendPQExpBufferStr(errbuf, actx->errbuf.data);
 
 	if (actx->curl_err[0])
 	{
 		size_t		len;
 
-		appendPQExpBuffer(&conn->errorMessage,
-						  " (libcurl: %s)", actx->curl_err);
+		appendPQExpBuffer(errbuf, " (libcurl: %s)", actx->curl_err);
 
 		/* Sometimes libcurl adds a newline to the error buffer. :( */
-		len = conn->errorMessage.len;
-		if (len >= 2 && conn->errorMessage.data[len - 2] == '\n')
+		len = errbuf->len;
+		if (len >= 2 && errbuf->data[len - 2] == '\n')
 		{
-			conn->errorMessage.data[len - 2] = ')';
-			conn->errorMessage.data[len - 1] = '\0';
-			conn->errorMessage.len--;
+			errbuf->data[len - 2] = ')';
+			errbuf->data[len - 1] = '\0';
+			errbuf->len--;
 		}
 	}
 
-	appendPQExpBufferChar(&conn->errorMessage, '\n');
+	appendPQExpBufferChar(errbuf, '\n');
 
 	return PGRES_POLLING_FAILED;
 }
diff --git a/src/interfaces/libpq-oauth/oauth-curl.h b/src/interfaces/libpq-oauth/oauth-curl.h
new file mode 100644
index 00000000000..248d0424ad0
--- /dev/null
+++ b/src/interfaces/libpq-oauth/oauth-curl.h
@@ -0,0 +1,24 @@
+/*-------------------------------------------------------------------------
+ *
+ * oauth-curl.h
+ *
+ *	  Definitions for OAuth Device Authorization module
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/interfaces/libpq-oauth/oauth-curl.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef OAUTH_CURL_H
+#define OAUTH_CURL_H
+
+#include "libpq-fe.h"
+
+/* Exported async-auth callbacks. */
+extern PGDLLEXPORT PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn);
+extern PGDLLEXPORT void pg_fe_cleanup_oauth_flow(PGconn *conn);
+
+#endif							/* OAUTH_CURL_H */
diff --git a/src/interfaces/libpq-oauth/oauth-utils.c b/src/interfaces/libpq-oauth/oauth-utils.c
new file mode 100644
index 00000000000..1f85a6b0479
--- /dev/null
+++ b/src/interfaces/libpq-oauth/oauth-utils.c
@@ -0,0 +1,202 @@
+/*-------------------------------------------------------------------------
+ *
+ * oauth-utils.c
+ *
+ *	  "Glue" helpers providing a copy of some internal APIs from libpq. At
+ *	  some point in the future, we might be able to deduplicate.
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq-oauth/oauth-utils.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#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;
+
+pgthreadlock_t pg_g_threadlock;
+conn_errorMessage_func conn_errorMessage;
+
+/*-
+ * Initializes libpq-oauth by setting necessary callbacks.
+ *
+ * The current implementation relies on the following private implementation
+ * details of libpq:
+ *
+ * - pg_g_threadlock: protects libcurl initialization if the underlying Curl
+ *   installation is not threadsafe
+ *
+ * - 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().
+ */
+void
+libpq_oauth_init(pgthreadlock_t threadlock_impl,
+				 libpq_gettext_func gettext_impl,
+				 conn_errorMessage_func errmsg_impl)
+{
+	pg_g_threadlock = threadlock_impl;
+	libpq_gettext_impl = gettext_impl;
+	conn_errorMessage = errmsg_impl;
+}
+
+/*
+ * Append a formatted string to the error message buffer of the given
+ * connection, after translating it.  This is a copy of libpq's internal API.
+ */
+void
+libpq_append_conn_error(PGconn *conn, const char *fmt,...)
+{
+	int			save_errno = errno;
+	bool		done;
+	va_list		args;
+	PQExpBuffer errorMessage = conn_errorMessage(conn);
+
+	Assert(fmt[strlen(fmt) - 1] != '\n');
+
+	if (PQExpBufferBroken(errorMessage))
+		return;					/* already failed */
+
+	/* Loop in case we have to retry after enlarging the buffer. */
+	do
+	{
+		errno = save_errno;
+		va_start(args, fmt);
+		done = appendPQExpBufferVA(errorMessage, libpq_gettext(fmt), args);
+		va_end(args);
+	} while (!done);
+
+	appendPQExpBufferChar(errorMessage, '\n');
+}
+
+#ifdef ENABLE_NLS
+
+/*
+ * A shim that defers to the actual libpq_gettext().
+ */
+char *
+libpq_gettext(const char *msgid)
+{
+	if (!libpq_gettext_impl)
+	{
+		/*
+		 * Possible if the libpq build didn't enable NLS but the libpq-oauth
+		 * build did. That's an odd mismatch, but we can handle it.
+		 *
+		 * Note that callers of libpq_gettext() have to treat the return value
+		 * as if it were const, because builds without NLS simply pass through
+		 * their argument.
+		 */
+		return unconstify(char *, msgid);
+	}
+
+	return libpq_gettext_impl(msgid);
+}
+
+#endif							/* ENABLE_NLS */
+
+/*
+ * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment.
+ */
+bool
+oauth_unsafe_debugging_enabled(void)
+{
+	const char *env = getenv("PGOAUTHDEBUG");
+
+	return (env && strcmp(env, "UNSAFE") == 0);
+}
+
+/*
+ * Duplicate SOCK_ERRNO* definitions from libpq-int.h, for use by
+ * pq_block/reset_sigpipe().
+ */
+#ifdef WIN32
+#define SOCK_ERRNO (WSAGetLastError())
+#define SOCK_ERRNO_SET(e) WSASetLastError(e)
+#else
+#define SOCK_ERRNO errno
+#define SOCK_ERRNO_SET(e) (errno = (e))
+#endif
+
+/*
+ *	Block SIGPIPE for this thread. This is a copy of libpq's internal API.
+ */
+int
+pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending)
+{
+	sigset_t	sigpipe_sigset;
+	sigset_t	sigset;
+
+	sigemptyset(&sigpipe_sigset);
+	sigaddset(&sigpipe_sigset, SIGPIPE);
+
+	/* Block SIGPIPE and save previous mask for later reset */
+	SOCK_ERRNO_SET(pthread_sigmask(SIG_BLOCK, &sigpipe_sigset, osigset));
+	if (SOCK_ERRNO)
+		return -1;
+
+	/* We can have a pending SIGPIPE only if it was blocked before */
+	if (sigismember(osigset, SIGPIPE))
+	{
+		/* Is there a pending SIGPIPE? */
+		if (sigpending(&sigset) != 0)
+			return -1;
+
+		if (sigismember(&sigset, SIGPIPE))
+			*sigpipe_pending = true;
+		else
+			*sigpipe_pending = false;
+	}
+	else
+		*sigpipe_pending = false;
+
+	return 0;
+}
+
+/*
+ *	Discard any pending SIGPIPE and reset the signal mask. This is a copy of
+ *	libpq's internal API.
+ */
+void
+pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe)
+{
+	int			save_errno = SOCK_ERRNO;
+	int			signo;
+	sigset_t	sigset;
+
+	/* Clear SIGPIPE only if none was pending */
+	if (got_epipe && !sigpipe_pending)
+	{
+		if (sigpending(&sigset) == 0 &&
+			sigismember(&sigset, SIGPIPE))
+		{
+			sigset_t	sigpipe_sigset;
+
+			sigemptyset(&sigpipe_sigset);
+			sigaddset(&sigpipe_sigset, SIGPIPE);
+
+			sigwait(&sigpipe_sigset, &signo);
+		}
+	}
+
+	/* Restore saved block mask */
+	pthread_sigmask(SIG_SETMASK, osigset, NULL);
+
+	SOCK_ERRNO_SET(save_errno);
+}
diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h
new file mode 100644
index 00000000000..e2a9d01237d
--- /dev/null
+++ b/src/interfaces/libpq-oauth/oauth-utils.h
@@ -0,0 +1,38 @@
+/*-------------------------------------------------------------------------
+ *
+ * oauth-utils.h
+ *
+ *	  Definitions providing missing libpq internal APIs
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/interfaces/libpq-oauth/oauth-utils.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef OAUTH_UTILS_H
+#define OAUTH_UTILS_H
+
+#include "libpq-fe.h"
+#include "pqexpbuffer.h"
+
+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);
+
+/* Callback to safely obtain conn->errorMessage from a PGconn. */
+extern conn_errorMessage_func conn_errorMessage;
+
+/* 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);
+
+#endif							/* OAUTH_UTILS_H */
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index 90b0b65db6f..c6fe5fec7f6 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -31,7 +31,6 @@ endif
 
 OBJS = \
 	$(WIN32RES) \
-	fe-auth-oauth.o \
 	fe-auth-scram.o \
 	fe-cancel.o \
 	fe-connect.o \
@@ -64,9 +63,11 @@ OBJS += \
 	fe-secure-gssapi.o
 endif
 
-ifeq ($(with_libcurl),yes)
-OBJS += fe-auth-oauth-curl.o
-endif
+# The OAuth implementation differs depending on the type of library being built.
+OBJS_STATIC = fe-auth-oauth.o
+
+fe-auth-oauth_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH
+OBJS_SHLIB = fe-auth-oauth_shlib.o
 
 ifeq ($(PORTNAME), cygwin)
 override shlib = cyg$(NAME)$(DLSUFFIX)
@@ -86,7 +87,7 @@ endif
 # that are built correctly for use in a shlib.
 SHLIB_LINK_INTERNAL = -lpgcommon_shlib -lpgport_shlib
 ifneq ($(PORTNAME), win32)
-SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lcurl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS)
+SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS)
 else
 SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi32 -lssl -lsocket -lnsl -lresolv -lintl -lm $(PTHREAD_LIBS), $(LIBS)) $(LDAP_LIBS_FE)
 endif
@@ -101,12 +102,26 @@ ifeq ($(with_ssl),openssl)
 PKG_CONFIG_REQUIRES_PRIVATE = libssl, libcrypto
 endif
 
+ifeq ($(with_libcurl),yes)
+# libpq.so doesn't link against libcurl, but libpq.a needs libpq-oauth, and
+# libpq-oauth needs libcurl. Put both into *.private.
+PKG_CONFIG_REQUIRES_PRIVATE += libcurl
+%.pc: override SHLIB_LINK_INTERNAL += -lpq-oauth
+endif
+
 all: all-lib libpq-refs-stamp
 
 # Shared library stuff
 include $(top_srcdir)/src/Makefile.shlib
 backend_src = $(top_srcdir)/src/backend
 
+# Add shlib-/stlib-specific objects.
+$(shlib): override OBJS += $(OBJS_SHLIB)
+$(shlib): $(OBJS_SHLIB)
+
+$(stlib): override OBJS += $(OBJS_STATIC)
+$(stlib): $(OBJS_STATIC)
+
 # Check for functions that libpq must not call, currently just exit().
 # (Ideally we'd reject abort() too, but there are various scenarios where
 # build toolchains insert abort() calls, e.g. to implement assert().)
@@ -115,8 +130,6 @@ backend_src = $(top_srcdir)/src/backend
 # which seems to insert references to that even in pure C code. Excluding
 # __tsan_func_exit is necessary when using ThreadSanitizer data race detector
 # which use this function for instrumentation of function exit.
-# libcurl registers an exit handler in the memory debugging code when running
-# with LeakSanitizer.
 # Skip the test when profiling, as gcc may insert exit() calls for that.
 # Also skip the test on platforms where libpq infrastructure may be provided
 # by statically-linked libraries, as we can't expect them to honor this
@@ -124,7 +137,7 @@ backend_src = $(top_srcdir)/src/backend
 libpq-refs-stamp: $(shlib)
 ifneq ($(enable_coverage), yes)
 ifeq (,$(filter solaris,$(PORTNAME)))
-	@if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit -e _atexit | grep exit; then \
+	@if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit | grep exit; then \
 		echo 'libpq must not be calling any function which invokes exit'; exit 1; \
 	fi
 endif
@@ -138,6 +151,11 @@ fe-misc.o: fe-misc.c $(top_builddir)/src/port/pg_config_paths.h
 $(top_builddir)/src/port/pg_config_paths.h:
 	$(MAKE) -C $(top_builddir)/src/port pg_config_paths.h
 
+# Use src/common/Makefile's trick for tracking dependencies of shlib-specific
+# objects.
+%_shlib.o: %.c %.o
+	$(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@
+
 install: all installdirs install-lib
 	$(INSTALL_DATA) $(srcdir)/libpq-fe.h '$(DESTDIR)$(includedir)'
 	$(INSTALL_DATA) $(srcdir)/libpq-events.h '$(DESTDIR)$(includedir)'
@@ -171,6 +189,6 @@ uninstall: uninstall-lib
 clean distclean: clean-lib
 	$(MAKE) -C test $@
 	rm -rf tmp_check
-	rm -f $(OBJS) pthread.h libpq-refs-stamp
+	rm -f $(OBJS) $(OBJS_SHLIB) $(OBJS_STATIC) pthread.h libpq-refs-stamp
 # Might be left over from a Win32 client-only build
 	rm -f pg_config_paths.h
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index d5143766858..0625cf39e9a 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -210,3 +210,4 @@ PQsetAuthDataHook         207
 PQgetAuthDataHook         208
 PQdefaultAuthDataHook     209
 PQfullProtocolVersion     210
+appendPQExpBufferVA       211
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index cf1a25e2ccc..ccdd9139cf1 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -15,6 +15,10 @@
 
 #include "postgres_fe.h"
 
+#ifdef USE_DYNAMIC_OAUTH
+#include <dlfcn.h>
+#endif
+
 #include "common/base64.h"
 #include "common/hmac.h"
 #include "common/jsonapi.h"
@@ -22,6 +26,7 @@
 #include "fe-auth.h"
 #include "fe-auth-oauth.h"
 #include "mb/pg_wchar.h"
+#include "pg_config_paths.h"
 
 /* The exported OAuth callback mechanism. */
 static void *oauth_init(PGconn *conn, const char *password,
@@ -721,6 +726,186 @@ cleanup_user_oauth_flow(PGconn *conn)
 	state->async_ctx = NULL;
 }
 
+/*-------------
+ * Builtin Flow
+ *
+ * There are three potential implementations of use_builtin_flow:
+ *
+ * 1) If the OAuth client is disabled at configuration time, return false.
+ *    Dependent clients must provide their own flow.
+ * 2) If the OAuth client is enabled and USE_DYNAMIC_OAUTH is defined, dlopen()
+ *    the libpq-oauth plugin and use its implementation.
+ * 3) Otherwise, use flow callbacks that are statically linked into the
+ *    executable.
+ */
+
+#if !defined(USE_LIBCURL)
+
+/*
+ * This configuration doesn't support the builtin flow.
+ */
+
+bool
+use_builtin_flow(PGconn *conn, fe_oauth_state *state)
+{
+	return false;
+}
+
+#elif defined(USE_DYNAMIC_OAUTH)
+
+/*
+ * Use the builtin flow in the libpq-oauth plugin, which is loaded at runtime.
+ */
+
+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.
+ */
+static PQExpBuffer
+conn_errorMessage(PGconn *conn)
+{
+	return &conn->errorMessage;
+}
+
+/*
+ * Loads the libpq-oauth plugin via dlopen(), initializes it, and plugs its
+ * callbacks into the connection's async auth handlers.
+ *
+ * Failure to load here results in a relatively quiet connection error, to
+ * handle the use case where the build supports loading a flow but a user does
+ * not want to install it. Troubleshooting of linker/loader failures can be done
+ * via PGOAUTHDEBUG.
+ */
+bool
+use_builtin_flow(PGconn *conn, fe_oauth_state *state)
+{
+	static bool initialized = false;
+	static pthread_mutex_t init_mutex = PTHREAD_MUTEX_INITIALIZER;
+	int			lockerr;
+
+	void		(*init) (pgthreadlock_t threadlock,
+						 libpq_gettext_func gettext_impl,
+						 conn_errorMessage_func errmsg_impl);
+	PostgresPollingStatusType (*flow) (PGconn *conn);
+	void		(*cleanup) (PGconn *conn);
+
+	/*
+	 * On macOS only, load the module using its absolute install path; the
+	 * standard search behavior is not very helpful for this use case. Unlike
+	 * on other platforms, DYLD_LIBRARY_PATH is used as a fallback even with
+	 * absolute paths (modulo SIP effects), so tests can continue to work.
+	 *
+	 * On the other platforms, load the module using only the basename, to
+	 * rely on the runtime linker's standard search behavior.
+	 */
+	const char *const module_name =
+#if defined(__darwin__)
+		LIBDIR "/libpq-oauth-" PG_MAJORVERSION "-" PG_MINORVERSION DLSUFFIX;
+#else
+		"libpq-oauth-" PG_MAJORVERSION "-" PG_MINORVERSION DLSUFFIX;
+#endif
+
+	state->builtin_flow = dlopen(module_name, RTLD_NOW | RTLD_LOCAL);
+	if (!state->builtin_flow)
+	{
+		/*
+		 * For end users, this probably isn't an error condition, it just
+		 * means the flow isn't installed. Developers and package maintainers
+		 * may want to debug this via the PGOAUTHDEBUG envvar, though.
+		 *
+		 * Note that POSIX dlerror() isn't guaranteed to be threadsafe.
+		 */
+		if (oauth_unsafe_debugging_enabled())
+			fprintf(stderr, "failed dlopen for libpq-oauth: %s\n", dlerror());
+
+		return false;
+	}
+
+	if ((init = dlsym(state->builtin_flow, "libpq_oauth_init")) == NULL
+		|| (flow = dlsym(state->builtin_flow, "pg_fe_run_oauth_flow")) == NULL
+		|| (cleanup = dlsym(state->builtin_flow, "pg_fe_cleanup_oauth_flow")) == NULL)
+	{
+		/*
+		 * This is more of an error condition than the one above, but due to
+		 * the dlerror() threadsafety issue, lock it behind PGOAUTHDEBUG too.
+		 */
+		if (oauth_unsafe_debugging_enabled())
+			fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
+
+		dlclose(state->builtin_flow);
+		return false;
+	}
+
+	/*
+	 * Past this point, we do not unload the module. It stays in the process
+	 * permanently.
+	 */
+
+	/*
+	 * We need to inject necessary function pointers into the module. This
+	 * only needs to be done once -- even if the pointers are constant,
+	 * assigning them while another thread is executing the flows feels like
+	 * tempting fate.
+	 */
+	if ((lockerr = pthread_mutex_lock(&init_mutex)) != 0)
+	{
+		/* Should not happen... but don't continue if it does. */
+		Assert(false);
+
+		libpq_append_conn_error(conn, "failed to lock mutex (%d)", lockerr);
+		return false;
+	}
+
+	if (!initialized)
+	{
+		init(pg_g_threadlock,
+#ifdef ENABLE_NLS
+			 libpq_gettext,
+#else
+			 NULL,
+#endif
+			 conn_errorMessage);
+
+		initialized = true;
+	}
+
+	pthread_mutex_unlock(&init_mutex);
+
+	/* Set our asynchronous callbacks. */
+	conn->async_auth = flow;
+	conn->cleanup_async_auth = cleanup;
+
+	return true;
+}
+
+#else
+
+/*
+ * Use the builtin flow in libpq-oauth.a (see libpq-oauth/oauth-curl.h).
+ */
+
+extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn);
+extern void pg_fe_cleanup_oauth_flow(PGconn *conn);
+
+bool
+use_builtin_flow(PGconn *conn, fe_oauth_state *state)
+{
+	/* Set our asynchronous callbacks. */
+	conn->async_auth = pg_fe_run_oauth_flow;
+	conn->cleanup_async_auth = pg_fe_cleanup_oauth_flow;
+
+	return true;
+}
+
+#endif							/* USE_LIBCURL */
+
+
 /*
  * Chooses an OAuth client flow for the connection, which will retrieve a Bearer
  * token for presentation to the server.
@@ -792,18 +977,10 @@ setup_token_request(PGconn *conn, fe_oauth_state *state)
 		libpq_append_conn_error(conn, "user-defined OAuth flow failed");
 		goto fail;
 	}
-	else
+	else if (!use_builtin_flow(conn, state))
 	{
-#if USE_LIBCURL
-		/* Hand off to our built-in OAuth flow. */
-		conn->async_auth = pg_fe_run_oauth_flow;
-		conn->cleanup_async_auth = pg_fe_cleanup_oauth_flow;
-
-#else
-		libpq_append_conn_error(conn, "no custom OAuth flows are available, and libpq was not built with libcurl support");
+		libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)");
 		goto fail;
-
-#endif
 	}
 
 	return true;
diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h
index 3f1a7503a01..687e664475f 100644
--- a/src/interfaces/libpq/fe-auth-oauth.h
+++ b/src/interfaces/libpq/fe-auth-oauth.h
@@ -33,12 +33,13 @@ typedef struct
 
 	PGconn	   *conn;
 	void	   *async_ctx;
+
+	void	   *builtin_flow;
 } fe_oauth_state;
 
-extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn);
-extern void pg_fe_cleanup_oauth_flow(PGconn *conn);
 extern void pqClearOAuthToken(PGconn *conn);
 extern bool oauth_unsafe_debugging_enabled(void);
+extern bool use_builtin_flow(PGconn *conn, fe_oauth_state *state);
 
 /* Mechanisms in fe-auth-oauth.c */
 extern const pg_fe_sasl_mech pg_oauth_mech;
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 292fecf3320..a74e885b169 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -38,10 +38,6 @@ if gssapi.found()
   )
 endif
 
-if libcurl.found()
-  libpq_sources += files('fe-auth-oauth-curl.c')
-endif
-
 export_file = custom_target('libpq.exports',
   kwargs: gen_export_kwargs,
 )
@@ -50,6 +46,9 @@ export_file = custom_target('libpq.exports',
 libpq_inc = include_directories('.', '../../port')
 libpq_c_args = ['-DSO_MAJOR_VERSION=5']
 
+# The OAuth implementation differs depending on the type of library being built.
+libpq_so_c_args = ['-DUSE_DYNAMIC_OAUTH']
+
 # Not using both_libraries() here as
 # 1) resource files should only be in the shared library
 # 2) we want the .pc file to include a dependency to {pgport,common}_static for
@@ -70,7 +69,7 @@ libpq_st = static_library('libpq',
 libpq_so = shared_library('libpq',
   libpq_sources + libpq_so_sources,
   include_directories: [libpq_inc, postgres_inc],
-  c_args: libpq_c_args,
+  c_args: libpq_c_args + libpq_so_c_args,
   c_pch: pch_postgres_fe_h,
   version: '5.' + pg_version_major.to_string(),
   soversion: host_system != 'windows' ? '5' : '',
@@ -86,12 +85,26 @@ libpq = declare_dependency(
   include_directories: [include_directories('.')]
 )
 
+private_deps = [
+  frontend_stlib_code,
+  libpq_deps,
+]
+
+if oauth_flow_supported
+  # libpq.so doesn't link against libcurl, but libpq.a needs libpq-oauth, and
+  # libpq-oauth needs libcurl. Put both into *.private.
+  private_deps += [
+    libpq_oauth_deps,
+    '-lpq-oauth',
+  ]
+endif
+
 pkgconfig.generate(
   name: 'libpq',
   description: 'PostgreSQL libpq library',
   url: pg_url,
   libraries: libpq,
-  libraries_private: [frontend_stlib_code, libpq_deps],
+  libraries_private: private_deps,
 )
 
 install_headers(
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index ae761265852..b87df277d93 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -13,15 +13,21 @@ GETTEXT_FILES    = fe-auth.c \
                    fe-secure-common.c \
                    fe-secure-gssapi.c \
                    fe-secure-openssl.c \
-                   win32.c
-GETTEXT_TRIGGERS = libpq_append_conn_error:2 \
+                   win32.c \
+                   ../libpq-oauth/oauth-curl.c \
+                   ../libpq-oauth/oauth-utils.c
+GETTEXT_TRIGGERS = actx_error:2 \
+                   libpq_append_conn_error:2 \
                    libpq_append_error:2 \
                    libpq_gettext \
                    libpq_ngettext:1,2 \
+                   oauth_parse_set_error:2 \
                    pqInternalNotice:2
-GETTEXT_FLAGS    = libpq_append_conn_error:2:c-format \
+GETTEXT_FLAGS    = actx_error:2:c-format \
+                   libpq_append_conn_error:2:c-format \
                    libpq_append_error:2:c-format \
                    libpq_gettext:1:pass-c-format \
                    libpq_ngettext:1:pass-c-format \
                    libpq_ngettext:2:pass-c-format \
+                   oauth_parse_set_error:2:c-format \
                    pqInternalNotice:2:c-format
diff --git a/src/makefiles/meson.build b/src/makefiles/meson.build
index e3adb5d8dc4..48d01a54dc6 100644
--- a/src/makefiles/meson.build
+++ b/src/makefiles/meson.build
@@ -204,6 +204,8 @@ pgxs_empty = [
   'LIBNUMA_CFLAGS', 'LIBNUMA_LIBS',
 
   'LIBURING_CFLAGS', 'LIBURING_LIBS',
+
+  'LIBCURL_CPPFLAGS', 'LIBCURL_LDFLAGS', 'LIBCURL_LDLIBS',
 ]
 
 if host_system == 'windows' and cc.get_argument_syntax() != 'msvc'
diff --git a/src/test/modules/oauth_validator/meson.build b/src/test/modules/oauth_validator/meson.build
index 36d1b26369f..e190f9cf15a 100644
--- a/src/test/modules/oauth_validator/meson.build
+++ b/src/test/modules/oauth_validator/meson.build
@@ -78,7 +78,7 @@ tests += {
     ],
     'env': {
       'PYTHON': python.path(),
-      'with_libcurl': libcurl.found() ? 'yes' : 'no',
+      'with_libcurl': oauth_flow_supported ? 'yes' : 'no',
       'with_python': 'yes',
     },
   },
diff --git a/src/test/modules/oauth_validator/t/002_client.pl b/src/test/modules/oauth_validator/t/002_client.pl
index 8dd502f41e1..21d4acc1926 100644
--- a/src/test/modules/oauth_validator/t/002_client.pl
+++ b/src/test/modules/oauth_validator/t/002_client.pl
@@ -110,7 +110,7 @@ if ($ENV{with_libcurl} ne 'yes')
 		"fails without custom hook installed",
 		flags => ["--no-hook"],
 		expected_stderr =>
-		  qr/no custom OAuth flows are available, and libpq was not built with libcurl support/
+		  qr/no OAuth flows are available \(try installing the libpq-oauth package\)/
 	);
 }
 
-- 
2.34.1

Reply via email to