public inbox for [email protected]
help / color / mirror / Atom feedFrom: Jacob Champion <[email protected]>
To: Zsolt Parragi <[email protected]>
Cc: PostgreSQL Hackers <[email protected]>
Cc: Chao Li <[email protected]>
Subject: Re: [oauth] Stabilize the libpq-oauth ABI (and allow alternative implementations?)
Date: Tue, 10 Mar 2026 09:32:30 -0700
Message-ID: <CAOYmi+=Pr7AAdkcKXyLw3ycxcrjGKsOV2CTYVV2PKYQw9ecG0Q@mail.gmail.com> (raw)
In-Reply-To: <CAOYmi+mLaohk3FLbH9fKGmzN7yFGHs2Zefdyp31wbL1130puiA@mail.gmail.com>
References: <CAOYmi+mrGg+n_X2MOLgeWcj3v_M00gR8uz_D7mM8z=dX1JYVbg@mail.gmail.com>
<[email protected]>
<CAOYmi+mwzS4xfgomL1Lte+W1hRk+zahhZLnRMDJ8nYxoHUWt_w@mail.gmail.com>
<CAOYmi+mEU_q9sr1PMmE-4rLwFN=OjyndDwFZvpsMU3RNJLrM9g@mail.gmail.com>
<CAN4CZFPwdH_GMrappThju=t4m5n95Dse6ecTNwN=h4GpZLOMng@mail.gmail.com>
<CAOYmi+m-hphbx=CRfGSdcrHi-jL3OrZGJMTAPOYp=OK253hBTA@mail.gmail.com>
<CAOYmi+nnYr_Oe=KH2ZW6Ld88eZWv5P-s5ZtWOEh2KsPtMqOzbg@mail.gmail.com>
<CAOYmi+nCg5upBVOo_UCSjMfO=YMkZXcSEsgaADKXqerr5wahZQ@mail.gmail.com>
<CAOYmi+nRmt+K+ZVNztKFv=LuasDCCSsXLihZB6BEkVeroW8EvA@mail.gmail.com>
<CAN4CZFOMv=0Y1PZ8uw3xgJmEqt4D1dr1-yOq9jjZYTARZp7H9Q@mail.gmail.com>
<CAOYmi+mLaohk3FLbH9fKGmzN7yFGHs2Zefdyp31wbL1130puiA@mail.gmail.com>
On Fri, Mar 6, 2026 at 4:27 PM Jacob Champion
<[email protected]> wrote:
> I'll cherry-pick some of the -1 handling backwards in the patchset to
> handle this.
Done in v7-0001. Some of the improvements in the WIP patch were also
cherry-picked, and I fixed a stray comment bug. -0001 and -0002 are
next up for commit.
-0003 fills out the commit message and should be generally reviewable
now. -0004 adds the ability to LD_LIBRARY_PATH your way into flow
plugin development for the v20 cycle. -0005 becomes pretty much
useless other than for testing (and it should possible to adapt those
tests to -0004's implementation at some point).
Thanks,
--Jacob
1: 90f05c5d963 < -: ----------- libpq: Introduce PQAUTHDATA_OAUTH_BEARER_TOKEN_V2
2: d8fbfc8f7a0 ! 1: f5b861f7efd libpq-oauth: Use the PGoauthBearerRequestV2 API
@@ src/interfaces/libpq/fe-auth-oauth.c: run_user_oauth_flow(PGconn *conn)
/*
- * Cleanup callback for the async user flow. Delegates most of its job to
+ * Cleanup callback for the async flow. Delegates most of its job to
- * PGoauthBearerRequestV2.cleanup(), then disconnects the altsock and frees the
+ * PGoauthBearerRequest.cleanup(), then disconnects the altsock and frees the
* request itself.
+ *
+ * This is called either at the end of a successful authentication, or during
@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_st
+ || (start_flow = dlsym(state->builtin_flow, "pg_start_oauthbearer")) == NULL)
{
/*
- * This is more of an error condition than the one above, but due to
-@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_state *state)
+- * This is more of an error condition than the one above, but due to
+- * the dlerror() threadsafety issue, lock it behind PGOAUTHDEBUG too.
++ * This is more of an error condition than the one above, but the
++ * cause is still locked behind PGOAUTHDEBUG due to the dlerror()
++ * threadsafety issue.
+ */
+ if (oauth_unsafe_debugging_enabled())
fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
dlclose(state->builtin_flow);
- return false;
-+ return 0;
++ state->builtin_flow = NULL;
++
++ request->error = libpq_gettext("could not find entry point for libpq-oauth");
++ return -1;
}
/*
@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_state *state)
+ /* Should not happen... but don't continue if it does. */
Assert(false);
- libpq_append_conn_error(conn, "failed to lock mutex (%d)", lockerr);
+- libpq_append_conn_error(conn, "failed to lock mutex (%d)", lockerr);
- return false;
-+ return 0;
++ appendPQExpBuffer(&conn->errorMessage,
++ "use_builtin_flow: failed to lock mutex (%d)\n",
++ lockerr);
++
++ request->error = ""; /* satisfy report_flow_error() */
++ return -1;
}
if (!initialized)
@@ src/interfaces/libpq/fe-auth-oauth.c: setup_token_request(PGconn *conn, fe_oauth
+ conn->async_auth = run_oauth_flow;
+ conn->cleanup_async_auth = cleanup_oauth_flow;
state->async_ctx = request_copy;
- }
- else if (res < 0)
- {
+- }
+- else if (res < 0)
+- {
- report_user_flow_error(conn, &request);
-+ report_flow_error(conn, &request);
- goto fail;
- }
+- goto fail;
+- }
- else if (!use_builtin_flow(conn, state))
+- {
+- libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)");
+- goto fail;
++
++ return true;
+ }
+
+- return true;
++ /*
++ * Failure cases: either we tried to set up a flow and failed, or there
++ * was no flow to try.
++ */
++ if (res < 0)
++ report_flow_error(conn, &request);
+ else
- {
- libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)");
- goto fail;
++ libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)");
+
+ fail:
+ if (request.v1.cleanup)
3: 96bd5970668 = 2: f59d6431b04 libpq-oauth: Never link against libpq's encoding functions
4: 0e7fae1e152 ! 3: 75ac7ddc120 WIP: test out poisoning
@@ Metadata
Author: Jacob Champion <[email protected]>
## Commit message ##
- WIP: test out poisoning
+ libpq: Poison the v2 part of a v1 Bearer request
+
+ The new PGoauthBearerRequestV2 API (which has similarities to the
+ "subclass" pointer architecture in use by the backend, for Nodes)
+ carries the risk of a developer ignoring the type of hook in use and
+ just casting directly to the V2 struct. This will appear to work fine in
+ 19, but crash (or worse) when speaking to libpq 18.
+
+ However, we're in a unique position to catch this problem, because we
+ have tight control over the struct. Add poisoning code to the v1 path
+ which does the following:
+
+ - masks the v2 request->issuer pointer, to hopefully point at nonsense
+ memory
+ - abort()s if the v2 request->error is assigned by the hook
+ - attempts to cover both with VALGRIND_MAKE_MEM_NOACCESS for the
+ duration of the callback (a potential AddressSanitizer implementation
+ is left for future work)
+
+ The struct is unpoisoned after the call, so we can switch back to the v2
+ internal implementation when necessary.
+
+ Discussion: https://postgr.es/m/CAOYmi%2BmrGg%2Bn_X2MOLgeWcj3v_M00gR8uz_D7mM8z%3DdX1JYVbg%40mail.gmail.com
## src/interfaces/libpq/fe-auth-oauth.h ##
@@ src/interfaces/libpq/fe-auth-oauth.h: typedef struct
@@ src/interfaces/libpq/fe-auth-oauth.c: setup_token_request(PGconn *conn, fe_oauth
}
@@ src/interfaces/libpq/fe-auth-oauth.c: setup_token_request(PGconn *conn, fe_oauth_state *state)
- return true;
+ libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)");
fail:
- if (request.v1.cleanup)
-: ----------- > 4: 7a43398c90a libpq: Allow developers to reimplement libpq-oauth
5: fbb89bcd980 ! 5: b3daf3140f8 WIP: Introduce third-party OAuth flow plugins?
@@ Commit message
This experimental commit promotes the pg_start_oauthbearer API to a
public header (libpq-oauth.h) and adds a PGOAUTHMODULE environment
variable that overrides the load path for the plugin, allowing users to
- provide their own. The libpq_oauth_init function is now optional, and
- will remain undocumented. (Modules that don't provide it are marked as
- user-defined.)
+ provide their own. It's probably not a good solution; it provides almost
+ nothing over LD_LIBRARY_PATH (other than not being LD_LIBRARY_PATH).
This is a relatively small amount of implementation change, but
unfortunately the tests have a large amount of code motion to be able to
@@ src/interfaces/libpq-oauth/oauth-curl.h (deleted)
-
-#endif /* OAUTH_CURL_H */
- ## src/interfaces/libpq/fe-auth-oauth.h ##
-@@ src/interfaces/libpq/fe-auth-oauth.h: typedef struct
-
- bool v1;
- bool builtin;
-- void *builtin_flow;
-+ void *flow_module;
- } fe_oauth_state;
-
- extern void pqClearOAuthToken(PGconn *conn);
-
## src/interfaces/libpq/libpq-oauth.h (new) ##
@@
+/*-------------------------------------------------------------------------
@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_st
"libpq-oauth" DLSUFFIX;
#endif
-- state->builtin_flow = dlopen(module_name, RTLD_NOW | RTLD_LOCAL);
-- if (!state->builtin_flow)
+ /*-
+ * Additionally, the user may override the module path explicitly to be
+ * able to provide their own module, via PGOAUTHMODULE.
@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_st
+ else
+ state->builtin = true;
+
-+ state->flow_module = dlopen(module_name, RTLD_NOW | RTLD_LOCAL);
-+ if (!state->flow_module)
+ state->flow_module = dlopen(module_name, RTLD_NOW | RTLD_LOCAL);
+ if (!state->flow_module)
{
/*
* For end users, this probably isn't an error condition, it just
@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_st
+ return -1;
}
-- if ((init = dlsym(state->builtin_flow, "libpq_oauth_init")) == NULL
-- || (start_flow = dlsym(state->builtin_flow, "pg_start_oauthbearer")) == NULL)
-+ if ((start_flow = dlsym(state->flow_module, "pg_start_oauthbearer")) == 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());
-+ fprintf(stderr, "failed dlsym for %s: %s\n", module_name, dlerror());
-+
-+ dlclose(state->flow_module);
-+ state->flow_module = NULL;
-+
-+ if (state->builtin)
-+ return 0;
-
-- dlclose(state->builtin_flow);
-- return 0;
-+ request->error = libpq_gettext("plugin entry point could not be located");
-+ return -1;
- }
-
- /*
-@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
- */
-
/*
-- * 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.
-+ * Our libpq-oauth.so provides a special initialization function for libpq
+ * Our libpq-oauth.so provides a special initialization function for libpq
+- * integration. If we don't find this, assume that a custom module is in
+- * use instead.
+ * integration. It's not a problem if we don't find this; it just means
+ * that a user-defined module is being used.
*/
-- if ((lockerr = pthread_mutex_lock(&init_mutex)) != 0)
-+ init = dlsym(state->flow_module, "libpq_oauth_init");
-+
-+ if (!init)
-+ state->builtin = false; /* adjust our error messages */
-+ else
- {
-- /* Should not happen... but don't continue if it does. */
-- Assert(false);
-+ /*
-+ * 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 0;
-- }
-+ appendPQExpBuffer(&conn->errorMessage,
-+ "use_builtin_flow: failed to lock mutex (%d)\n",
-+ lockerr);
-
-- if (!initialized)
-- {
-- init(
-+ request->error = ""; /* satisfy report_flow_error() */
-+ return -1;
-+ }
-+
-+ if (!initialized)
-+ {
-+ init(
- #ifdef ENABLE_NLS
-- libpq_gettext
-+ libpq_gettext
- #else
-- NULL
-+ NULL
- #endif
-- );
-+ );
+ init = dlsym(state->flow_module, "libpq_oauth_init");
+ if (!init)
+@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
+ * threadsafety issue.
+ */
+ if (oauth_unsafe_debugging_enabled())
+- fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
++ fprintf(stderr, "failed dlsym for %s: %s\n", module_name, dlerror());
-- initialized = true;
-- }
-+ initialized = true;
-+ }
+ dlclose(state->flow_module);
+ state->flow_module = NULL;
-- pthread_mutex_unlock(&init_mutex);
-+ pthread_mutex_unlock(&init_mutex);
-+ }
+- request->error = libpq_gettext("could not find entry point for libpq-oauth");
++ request->error = state->builtin ?
++ libpq_gettext("could not find entry point for libpq-oauth") :
++ libpq_gettext("could not find entry point for custom plugin");
+ return -1;
+ }
- return (start_flow(conn, request) == 0) ? 1 : -1;
- }
@@ src/interfaces/libpq/fe-auth-oauth.c: extern int pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request);
static int
use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *request)
@@ src/interfaces/libpq/fe-auth-oauth.c: setup_token_request(PGconn *conn, fe_oauth
if (res > 0)
{
-@@ src/interfaces/libpq/fe-auth-oauth.c: setup_token_request(PGconn *conn, fe_oauth_state *state)
- conn->async_auth = run_oauth_flow;
- conn->cleanup_async_auth = cleanup_oauth_flow;
- state->async_ctx = request_copy;
-+
-+ return true;
- }
-- else if (res < 0)
-- {
-+
-+ /*
-+ * Failure cases: either we tried to set up a flow and failed, or there
-+ * was no flow to try.
-+ */
-+ if (res < 0)
- report_flow_error(conn, &request);
-- goto fail;
-- }
- else
-- {
- libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)");
-- goto fail;
-- }
--
-- return true;
-
- fail:
- do_cleanup(state, &request);
## src/test/modules/oauth_validator/meson.build ##
@@ src/test/modules/oauth_validator/meson.build: test_install_libs += magic_validator
@@ src/test/modules/oauth_validator/t/002_client.pl: sub test
}
test(
-@@ src/test/modules/oauth_validator/t/002_client.pl: test(
+@@ src/test/modules/oauth_validator/t/002_client.pl: $common_connstr = "$base_connstr oauth_issuer=$issuer oauth_client_id=myID";
# Make sure the v1 hook continues to work.
test(
"v1 synchronous hook can provide a token",
Attachments:
[text/plain] since-v6.diff.txt (13.8K, 2-since-v6.diff.txt)
download | inline:
1: 90f05c5d963 < -: ----------- libpq: Introduce PQAUTHDATA_OAUTH_BEARER_TOKEN_V2
2: d8fbfc8f7a0 ! 1: f5b861f7efd libpq-oauth: Use the PGoauthBearerRequestV2 API
@@ src/interfaces/libpq/fe-auth-oauth.c: run_user_oauth_flow(PGconn *conn)
/*
- * Cleanup callback for the async user flow. Delegates most of its job to
+ * Cleanup callback for the async flow. Delegates most of its job to
- * PGoauthBearerRequestV2.cleanup(), then disconnects the altsock and frees the
+ * PGoauthBearerRequest.cleanup(), then disconnects the altsock and frees the
* request itself.
+ *
+ * This is called either at the end of a successful authentication, or during
@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_st
+ || (start_flow = dlsym(state->builtin_flow, "pg_start_oauthbearer")) == NULL)
{
/*
- * This is more of an error condition than the one above, but due to
-@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_state *state)
+- * This is more of an error condition than the one above, but due to
+- * the dlerror() threadsafety issue, lock it behind PGOAUTHDEBUG too.
++ * This is more of an error condition than the one above, but the
++ * cause is still locked behind PGOAUTHDEBUG due to the dlerror()
++ * threadsafety issue.
+ */
+ if (oauth_unsafe_debugging_enabled())
fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
dlclose(state->builtin_flow);
- return false;
-+ return 0;
++ state->builtin_flow = NULL;
++
++ request->error = libpq_gettext("could not find entry point for libpq-oauth");
++ return -1;
}
/*
@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_state *state)
+ /* Should not happen... but don't continue if it does. */
Assert(false);
- libpq_append_conn_error(conn, "failed to lock mutex (%d)", lockerr);
+- libpq_append_conn_error(conn, "failed to lock mutex (%d)", lockerr);
- return false;
-+ return 0;
++ appendPQExpBuffer(&conn->errorMessage,
++ "use_builtin_flow: failed to lock mutex (%d)\n",
++ lockerr);
++
++ request->error = ""; /* satisfy report_flow_error() */
++ return -1;
}
if (!initialized)
@@ src/interfaces/libpq/fe-auth-oauth.c: setup_token_request(PGconn *conn, fe_oauth
+ conn->async_auth = run_oauth_flow;
+ conn->cleanup_async_auth = cleanup_oauth_flow;
state->async_ctx = request_copy;
- }
- else if (res < 0)
- {
+- }
+- else if (res < 0)
+- {
- report_user_flow_error(conn, &request);
-+ report_flow_error(conn, &request);
- goto fail;
- }
+- goto fail;
+- }
- else if (!use_builtin_flow(conn, state))
+- {
+- libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)");
+- goto fail;
++
++ return true;
+ }
+
+- return true;
++ /*
++ * Failure cases: either we tried to set up a flow and failed, or there
++ * was no flow to try.
++ */
++ if (res < 0)
++ report_flow_error(conn, &request);
+ else
- {
- libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)");
- goto fail;
++ libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)");
+
+ fail:
+ if (request.v1.cleanup)
3: 96bd5970668 = 2: f59d6431b04 libpq-oauth: Never link against libpq's encoding functions
4: 0e7fae1e152 ! 3: 75ac7ddc120 WIP: test out poisoning
@@ Metadata
Author: Jacob Champion <[email protected]>
## Commit message ##
- WIP: test out poisoning
+ libpq: Poison the v2 part of a v1 Bearer request
+
+ The new PGoauthBearerRequestV2 API (which has similarities to the
+ "subclass" pointer architecture in use by the backend, for Nodes)
+ carries the risk of a developer ignoring the type of hook in use and
+ just casting directly to the V2 struct. This will appear to work fine in
+ 19, but crash (or worse) when speaking to libpq 18.
+
+ However, we're in a unique position to catch this problem, because we
+ have tight control over the struct. Add poisoning code to the v1 path
+ which does the following:
+
+ - masks the v2 request->issuer pointer, to hopefully point at nonsense
+ memory
+ - abort()s if the v2 request->error is assigned by the hook
+ - attempts to cover both with VALGRIND_MAKE_MEM_NOACCESS for the
+ duration of the callback (a potential AddressSanitizer implementation
+ is left for future work)
+
+ The struct is unpoisoned after the call, so we can switch back to the v2
+ internal implementation when necessary.
+
+ Discussion: https://postgr.es/m/CAOYmi%2BmrGg%2Bn_X2MOLgeWcj3v_M00gR8uz_D7mM8z%3DdX1JYVbg%40mail.gmail.com
## src/interfaces/libpq/fe-auth-oauth.h ##
@@ src/interfaces/libpq/fe-auth-oauth.h: typedef struct
@@ src/interfaces/libpq/fe-auth-oauth.c: setup_token_request(PGconn *conn, fe_oauth
}
@@ src/interfaces/libpq/fe-auth-oauth.c: setup_token_request(PGconn *conn, fe_oauth_state *state)
- return true;
+ libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)");
fail:
- if (request.v1.cleanup)
-: ----------- > 4: 7a43398c90a libpq: Allow developers to reimplement libpq-oauth
5: fbb89bcd980 ! 5: b3daf3140f8 WIP: Introduce third-party OAuth flow plugins?
@@ Commit message
This experimental commit promotes the pg_start_oauthbearer API to a
public header (libpq-oauth.h) and adds a PGOAUTHMODULE environment
variable that overrides the load path for the plugin, allowing users to
- provide their own. The libpq_oauth_init function is now optional, and
- will remain undocumented. (Modules that don't provide it are marked as
- user-defined.)
+ provide their own. It's probably not a good solution; it provides almost
+ nothing over LD_LIBRARY_PATH (other than not being LD_LIBRARY_PATH).
This is a relatively small amount of implementation change, but
unfortunately the tests have a large amount of code motion to be able to
@@ src/interfaces/libpq-oauth/oauth-curl.h (deleted)
-
-#endif /* OAUTH_CURL_H */
- ## src/interfaces/libpq/fe-auth-oauth.h ##
-@@ src/interfaces/libpq/fe-auth-oauth.h: typedef struct
-
- bool v1;
- bool builtin;
-- void *builtin_flow;
-+ void *flow_module;
- } fe_oauth_state;
-
- extern void pqClearOAuthToken(PGconn *conn);
-
## src/interfaces/libpq/libpq-oauth.h (new) ##
@@
+/*-------------------------------------------------------------------------
@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_st
"libpq-oauth" DLSUFFIX;
#endif
-- state->builtin_flow = dlopen(module_name, RTLD_NOW | RTLD_LOCAL);
-- if (!state->builtin_flow)
+ /*-
+ * Additionally, the user may override the module path explicitly to be
+ * able to provide their own module, via PGOAUTHMODULE.
@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_st
+ else
+ state->builtin = true;
+
-+ state->flow_module = dlopen(module_name, RTLD_NOW | RTLD_LOCAL);
-+ if (!state->flow_module)
+ state->flow_module = dlopen(module_name, RTLD_NOW | RTLD_LOCAL);
+ if (!state->flow_module)
{
/*
* For end users, this probably isn't an error condition, it just
@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_st
+ return -1;
}
-- if ((init = dlsym(state->builtin_flow, "libpq_oauth_init")) == NULL
-- || (start_flow = dlsym(state->builtin_flow, "pg_start_oauthbearer")) == NULL)
-+ if ((start_flow = dlsym(state->flow_module, "pg_start_oauthbearer")) == 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());
-+ fprintf(stderr, "failed dlsym for %s: %s\n", module_name, dlerror());
-+
-+ dlclose(state->flow_module);
-+ state->flow_module = NULL;
-+
-+ if (state->builtin)
-+ return 0;
-
-- dlclose(state->builtin_flow);
-- return 0;
-+ request->error = libpq_gettext("plugin entry point could not be located");
-+ return -1;
- }
-
- /*
-@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
- */
-
/*
-- * 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.
-+ * Our libpq-oauth.so provides a special initialization function for libpq
+ * Our libpq-oauth.so provides a special initialization function for libpq
+- * integration. If we don't find this, assume that a custom module is in
+- * use instead.
+ * integration. It's not a problem if we don't find this; it just means
+ * that a user-defined module is being used.
*/
-- if ((lockerr = pthread_mutex_lock(&init_mutex)) != 0)
-+ init = dlsym(state->flow_module, "libpq_oauth_init");
-+
-+ if (!init)
-+ state->builtin = false; /* adjust our error messages */
-+ else
- {
-- /* Should not happen... but don't continue if it does. */
-- Assert(false);
-+ /*
-+ * 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 0;
-- }
-+ appendPQExpBuffer(&conn->errorMessage,
-+ "use_builtin_flow: failed to lock mutex (%d)\n",
-+ lockerr);
-
-- if (!initialized)
-- {
-- init(
-+ request->error = ""; /* satisfy report_flow_error() */
-+ return -1;
-+ }
-+
-+ if (!initialized)
-+ {
-+ init(
- #ifdef ENABLE_NLS
-- libpq_gettext
-+ libpq_gettext
- #else
-- NULL
-+ NULL
- #endif
-- );
-+ );
+ init = dlsym(state->flow_module, "libpq_oauth_init");
+ if (!init)
+@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
+ * threadsafety issue.
+ */
+ if (oauth_unsafe_debugging_enabled())
+- fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
++ fprintf(stderr, "failed dlsym for %s: %s\n", module_name, dlerror());
-- initialized = true;
-- }
-+ initialized = true;
-+ }
+ dlclose(state->flow_module);
+ state->flow_module = NULL;
-- pthread_mutex_unlock(&init_mutex);
-+ pthread_mutex_unlock(&init_mutex);
-+ }
+- request->error = libpq_gettext("could not find entry point for libpq-oauth");
++ request->error = state->builtin ?
++ libpq_gettext("could not find entry point for libpq-oauth") :
++ libpq_gettext("could not find entry point for custom plugin");
+ return -1;
+ }
- return (start_flow(conn, request) == 0) ? 1 : -1;
- }
@@ src/interfaces/libpq/fe-auth-oauth.c: extern int pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request);
static int
use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *request)
@@ src/interfaces/libpq/fe-auth-oauth.c: setup_token_request(PGconn *conn, fe_oauth
if (res > 0)
{
-@@ src/interfaces/libpq/fe-auth-oauth.c: setup_token_request(PGconn *conn, fe_oauth_state *state)
- conn->async_auth = run_oauth_flow;
- conn->cleanup_async_auth = cleanup_oauth_flow;
- state->async_ctx = request_copy;
-+
-+ return true;
- }
-- else if (res < 0)
-- {
-+
-+ /*
-+ * Failure cases: either we tried to set up a flow and failed, or there
-+ * was no flow to try.
-+ */
-+ if (res < 0)
- report_flow_error(conn, &request);
-- goto fail;
-- }
- else
-- {
- libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)");
-- goto fail;
-- }
--
-- return true;
-
- fail:
- do_cleanup(state, &request);
## src/test/modules/oauth_validator/meson.build ##
@@ src/test/modules/oauth_validator/meson.build: test_install_libs += magic_validator
@@ src/test/modules/oauth_validator/t/002_client.pl: sub test
}
test(
-@@ src/test/modules/oauth_validator/t/002_client.pl: test(
+@@ src/test/modules/oauth_validator/t/002_client.pl: $common_connstr = "$base_connstr oauth_issuer=$issuer oauth_client_id=myID";
# Make sure the v1 hook continues to work.
test(
"v1 synchronous hook can provide a token",
[application/octet-stream] v7-0001-libpq-oauth-Use-the-PGoauthBearerRequestV2-API.patch (44.4K, 3-v7-0001-libpq-oauth-Use-the-PGoauthBearerRequestV2-API.patch)
download | inline diff:
From f5b861f7efd5328fe7a4a2d034ad5410ffb508b8 Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email protected]>
Date: Thu, 23 Oct 2025 12:24:20 -0700
Subject: [PATCH v7 1/5] libpq-oauth: Use the PGoauthBearerRequestV2 API
Switch the private libpq-oauth ABI to the public PGoauthBearerRequestV2
API. A huge amount of glue code can be removed as part of this, and several
code paths can be deduplicated. Additionally, the shared library no
longer needs to change its name for every major release; it's now just
"libpq-oauth.so".
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Discussion: https://postgr.es/m/CAOYmi%2BmrGg%2Bn_X2MOLgeWcj3v_M00gR8uz_D7mM8z%3DdX1JYVbg%40mail.gmail.com
---
src/interfaces/libpq-oauth/README | 67 ++---
src/interfaces/libpq-oauth/exports.txt | 3 +-
src/interfaces/libpq-oauth/meson.build | 4 +-
src/interfaces/libpq-oauth/Makefile | 9 +-
src/interfaces/libpq-oauth/oauth-curl.h | 6 +-
src/interfaces/libpq-oauth/oauth-utils.h | 43 +--
src/interfaces/libpq/fe-auth-oauth.h | 7 +-
src/interfaces/libpq-oauth/oauth-curl.c | 336 ++++++++++++++---------
src/interfaces/libpq-oauth/oauth-utils.c | 77 +-----
src/interfaces/libpq/fe-auth-oauth.c | 229 ++++++++-------
10 files changed, 353 insertions(+), 428 deletions(-)
diff --git a/src/interfaces/libpq-oauth/README b/src/interfaces/libpq-oauth/README
index 553962d644e..a27a8238d4b 100644
--- a/src/interfaces/libpq-oauth/README
+++ b/src/interfaces/libpq-oauth/README
@@ -10,48 +10,33 @@ results in a failed connection.
= 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.
-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.
-
-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:
-
-- void libpq_oauth_init(pgthreadlock_t threadlock,
- libpq_gettext_func gettext_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);
-
-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.
+As of v19, this module ABI is public and cannot change incompatibly without also
+changing the entry points. Both libpq and libpq-oauth must gracefully handle
+situations where the other library is of a different release version, past or
+future, since upgrades to the libraries may happen in either order.
+
+(Don't assume that package version dependencies from libpq-oauth to libpq will
+simplify the situation! Since libpq delay-loads libpq-oauth, we still have to
+handle cases where a long-running client application has a libpq that's older
+than a newly upgraded plugin.)
+
+The module exports the following symbol:
+
+- int pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request);
+
+The module then behaves as if it had received a PQAUTHDATA_OAUTH_BEARER_TOKEN_V2
+request via the PQauthDataHook API, and it either fills in an existing token or
+populates the necessary callbacks for a token to be obtained asynchronously.
+(See the documentation for PGoauthBearerRequest.) The function returns zero on
+success and nonzero on failure.
+
+Additionally, libpq-oauth relies on libpq's libpq_gettext(), which must be
+injected by libpq using this initialization function before the flow is run:
+
+- void libpq_oauth_init(libpq_gettext_func gettext_impl);
= 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. (libpq.a and libpq-oauth.a must be
-part of the same build. Unlike the dynamic module, there are no translation
-shims provided.)
+flow is enabled, the application is expected to link against libpq-oauth.a to
+provide the necessary symbol, or else implement pg_start_oauthbearer() itself.
diff --git a/src/interfaces/libpq-oauth/exports.txt b/src/interfaces/libpq-oauth/exports.txt
index 6891a83dbf9..7bc12b860d7 100644
--- a/src/interfaces/libpq-oauth/exports.txt
+++ b/src/interfaces/libpq-oauth/exports.txt
@@ -1,4 +1,3 @@
# src/interfaces/libpq-oauth/exports.txt
libpq_oauth_init 1
-pg_fe_run_oauth_flow 2
-pg_fe_cleanup_oauth_flow 3
+pg_start_oauthbearer 2
diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build
index 27aca2bc324..685a00acf7a 100644
--- a/src/interfaces/libpq-oauth/meson.build
+++ b/src/interfaces/libpq-oauth/meson.build
@@ -38,10 +38,8 @@ endif
# 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)
-
if build_shared_lib
-libpq_oauth_so = shared_module(libpq_oauth_name,
+libpq_oauth_so = shared_module('libpq-oauth',
libpq_oauth_sources + libpq_oauth_so_sources,
include_directories: [libpq_oauth_inc, postgres_inc],
c_args: libpq_oauth_so_c_args,
diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile
index a5f2d65fcad..e90482566b1 100644
--- a/src/interfaces/libpq-oauth/Makefile
+++ b/src/interfaces/libpq-oauth/Makefile
@@ -16,13 +16,10 @@ 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)
-
-# Force the name "libpq-oauth" for both the static and shared libraries. The
-# staticlib doesn't need version information in its name.
+# SO_MAJOR_VERSION. This requires an explicit override for the shared library
+# name.
+NAME = pq-oauth
override shlib := lib$(NAME)$(DLSUFFIX)
-override stlib := libpq-oauth.a
override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port $(CPPFLAGS) $(LIBCURL_CPPFLAGS)
override CFLAGS += $(PTHREAD_CFLAGS)
diff --git a/src/interfaces/libpq-oauth/oauth-curl.h b/src/interfaces/libpq-oauth/oauth-curl.h
index 686e7b4df3f..1d4dd766217 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.h
+++ b/src/interfaces/libpq-oauth/oauth-curl.h
@@ -17,8 +17,8 @@
#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);
+/* Exported flow callback. */
+extern PGDLLEXPORT int pg_start_oauthbearer(PGconn *conn,
+ PGoauthBearerRequestV2 *request);
#endif /* OAUTH_CURL_H */
diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h
index 9f4d5b692d2..293e9936989 100644
--- a/src/interfaces/libpq-oauth/oauth-utils.h
+++ b/src/interfaces/libpq-oauth/oauth-utils.h
@@ -15,53 +15,13 @@
#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);
/* Initializes libpq-oauth. */
-extern PGDLLEXPORT void libpq_oauth_init(pgthreadlock_t threadlock,
- libpq_gettext_func gettext_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);
+extern PGDLLEXPORT void libpq_oauth_init(libpq_gettext_func gettext_impl);
/*
* Duplicated APIs, copied from libpq (primarily libpq-int.h, which we cannot
@@ -75,7 +35,6 @@ typedef enum
PG_BOOL_NO /* No (false) */
} PGTernaryBool;
-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);
diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h
index 5c8a24b76fa..511284614f7 100644
--- a/src/interfaces/libpq/fe-auth-oauth.h
+++ b/src/interfaces/libpq/fe-auth-oauth.h
@@ -27,11 +27,6 @@ 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;
@@ -39,12 +34,12 @@ typedef struct
PGconn *conn;
void *async_ctx;
+ bool builtin;
void *builtin_flow;
} fe_oauth_state;
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-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 2c147f98d0d..052ecd32da2 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -29,7 +29,6 @@
#endif
#include "common/jsonapi.h"
-#include "fe-auth-oauth.h"
#include "mb/pg_wchar.h"
#include "oauth-curl.h"
@@ -44,23 +43,10 @@
#else /* !USE_DYNAMIC_OAUTH */
-/*
- * Static builds may rely on PGconn offsets directly. Keep these aligned with
- * the bank of callbacks in oauth-utils.h.
- */
+/* Static builds may make use of libpq internals directly. */
+#include "fe-auth-oauth.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... */
@@ -227,6 +213,15 @@ enum OAuthStep
*/
struct async_ctx
{
+ /* relevant connection options cached from the PGconn */
+ char *client_id; /* oauth_client_id */
+ char *client_secret; /* oauth_client_secret (may be NULL) */
+
+ /* options cached from the PGoauthBearerRequest (we don't own these) */
+ const char *discovery_uri;
+ const char *issuer_id;
+ const char *scope;
+
enum OAuthStep step; /* where are we in the flow? */
int timerfd; /* descriptor for signaling async timeouts */
@@ -339,29 +334,72 @@ free_async_ctx(struct async_ctx *actx)
if (actx->timerfd >= 0)
close(actx->timerfd);
+ free(actx->client_id);
+ free(actx->client_secret);
+
free(actx);
}
/*
- * Release resources used for the asynchronous exchange and disconnect the
- * altsock.
- *
- * This is called either at the end of a successful authentication, or during
- * pqDropConnection(), so we won't leak resources even if PQconnectPoll() never
- * calls us back.
+ * Release resources used for the asynchronous exchange.
*/
-void
-pg_fe_cleanup_oauth_flow(PGconn *conn)
+static void
+pg_fe_cleanup_oauth_flow(PGconn *conn, PGoauthBearerRequest *request)
{
- fe_oauth_state *state = conn_sasl_state(conn);
+ struct async_ctx *actx = request->user;
+
+ /* request->cleanup is only set after actx has been allocated. */
+ Assert(actx);
- if (state->async_ctx)
+ free_async_ctx(actx);
+ request->user = NULL;
+
+ /* libpq has made its own copy of the token; clear ours now. */
+ if (request->token)
{
- free_async_ctx(state->async_ctx);
- state->async_ctx = NULL;
+ explicit_bzero(request->token, strlen(request->token));
+ free(request->token);
+ request->token = NULL;
}
+}
- set_conn_altsock(conn, PGINVALID_SOCKET);
+/*
+ * Builds an error message from actx and stores it in req->error. The allocation
+ * is backed by actx->work_data (which will be reset first).
+ */
+static void
+append_actx_error(PGoauthBearerRequestV2 *req, struct async_ctx *actx)
+{
+ PQExpBuffer errbuf = &actx->work_data;
+
+ resetPQExpBuffer(errbuf);
+
+ /*
+ * Assemble the three parts of our error: context, body, and detail. See
+ * also the documentation for struct async_ctx.
+ */
+ if (actx->errctx)
+ appendPQExpBuffer(errbuf, "%s: ", actx->errctx);
+
+ if (PQExpBufferDataBroken(actx->errbuf))
+ appendPQExpBufferStr(errbuf, libpq_gettext("out of memory"));
+ else
+ appendPQExpBufferStr(errbuf, actx->errbuf.data);
+
+ if (actx->curl_err[0])
+ {
+ appendPQExpBuffer(errbuf, " (libcurl: %s)", actx->curl_err);
+
+ /* Sometimes libcurl adds a newline to the error buffer. :( */
+ if (errbuf->len >= 2 && errbuf->data[errbuf->len - 2] == '\n')
+ {
+ errbuf->data[errbuf->len - 2] = ')';
+ errbuf->data[errbuf->len - 1] = '\0';
+ errbuf->len--;
+ }
+ }
+
+ req->error = errbuf->data;
}
/*
@@ -2197,7 +2235,7 @@ 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);
+ const char *oauth_issuer_id = actx->issuer_id;
Assert(oauth_issuer_id); /* ensured by setup_oauth_parameters() */
Assert(provider->issuer); /* ensured by parse_provider() */
@@ -2300,8 +2338,8 @@ 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);
+ const char *oauth_client_id = actx->client_id;
+ const char *oauth_client_secret = actx->client_secret;
bool success = false;
char *username = NULL;
@@ -2384,11 +2422,10 @@ cleanup:
static bool
start_device_authz(struct async_ctx *actx, PGconn *conn)
{
- const char *oauth_scope = conn_oauth_scope(conn);
+ const char *oauth_scope = actx->scope;
const char *device_authz_uri = actx->provider.device_authorization_endpoint;
PQExpBuffer work_buffer = &actx->work_data;
- 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. */
@@ -2476,7 +2513,6 @@ 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(conn)); /* ensured by setup_oauth_parameters() */
Assert(token_uri); /* ensured by parse_provider() */
Assert(device_code); /* ensured by parse_device_authz() */
@@ -2655,7 +2691,7 @@ prompt_user(struct async_ctx *actx, PGconn *conn)
* function will not try to reinitialize Curl on successive calls.
*/
static bool
-initialize_curl(PGconn *conn)
+initialize_curl(PGoauthBearerRequestV2 *req)
{
/*
* Don't let the compiler play tricks with this variable. In the
@@ -2689,8 +2725,7 @@ initialize_curl(PGconn *conn)
goto done;
else if (init_successful == PG_BOOL_NO)
{
- libpq_append_conn_error(conn,
- "curl_global_init previously failed during OAuth setup");
+ req->error = libpq_gettext("curl_global_init previously failed during OAuth setup");
goto done;
}
@@ -2708,8 +2743,7 @@ initialize_curl(PGconn *conn)
*/
if (curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32) != CURLE_OK)
{
- libpq_append_conn_error(conn,
- "curl_global_init failed during OAuth setup");
+ req->error = libpq_gettext("curl_global_init failed during OAuth setup");
init_successful = PG_BOOL_NO;
goto done;
}
@@ -2730,11 +2764,11 @@ initialize_curl(PGconn *conn)
* In a downgrade situation, the damage is already done. Curl global
* state may be corrupted. Be noisy.
*/
- libpq_append_conn_error(conn, "libcurl is no longer thread-safe\n"
- "\tCurl initialization was reported thread-safe when libpq\n"
- "\twas compiled, but the currently installed version of\n"
- "\tlibcurl reports that it is not. Recompile libpq against\n"
- "\tthe installed version of libcurl.");
+ req->error = libpq_gettext("libcurl is no longer thread-safe\n"
+ "\tCurl initialization was reported thread-safe when libpq\n"
+ "\twas compiled, but the currently installed version of\n"
+ "\tlibcurl reports that it is not. Recompile libpq against\n"
+ "\tthe installed version of libcurl.");
init_successful = PG_BOOL_NO;
goto done;
}
@@ -2764,54 +2798,16 @@ done:
* provider.
*/
static PostgresPollingStatusType
-pg_fe_run_oauth_flow_impl(PGconn *conn)
+pg_fe_run_oauth_flow_impl(PGconn *conn, PGoauthBearerRequestV2 *request,
+ int *altsock)
{
- fe_oauth_state *state = conn_sasl_state(conn);
- struct async_ctx *actx;
+ struct async_ctx *actx = request->v1.user;
char *oauth_token = NULL;
- PQExpBuffer errbuf;
-
- if (!initialize_curl(conn))
- return PGRES_POLLING_FAILED;
-
- if (!state->async_ctx)
- {
- /*
- * Create our asynchronous state, and hook it into the upper-level
- * OAuth state immediately, so any failures below won't leak the
- * context allocation.
- */
- actx = calloc(1, sizeof(*actx));
- if (!actx)
- {
- libpq_append_conn_error(conn, "out of memory");
- return PGRES_POLLING_FAILED;
- }
-
- actx->mux = PGINVALID_SOCKET;
- actx->timerfd = -1;
-
- /* Should we enable unsafe features? */
- actx->debugging = oauth_unsafe_debugging_enabled();
-
- state->async_ctx = actx;
-
- initPQExpBuffer(&actx->work_data);
- initPQExpBuffer(&actx->errbuf);
-
- if (!setup_multiplexer(actx))
- goto error_return;
-
- if (!setup_curl_handles(actx))
- goto error_return;
- }
-
- actx = state->async_ctx;
do
{
/* By default, the multiplexer is the altsock. Reassign as desired. */
- set_conn_altsock(conn, actx->mux);
+ *altsock = actx->mux;
switch (actx->step)
{
@@ -2876,7 +2872,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
if (!expired)
{
- set_conn_altsock(conn, actx->timerfd);
+ *altsock = actx->timerfd;
return PGRES_POLLING_READING;
}
@@ -2893,7 +2889,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
{
case OAUTH_STEP_INIT:
actx->errctx = libpq_gettext("failed to fetch OpenID discovery document");
- if (!start_discovery(actx, conn_oauth_discovery_uri(conn)))
+ if (!start_discovery(actx, actx->discovery_uri))
goto error_return;
actx->step = OAUTH_STEP_DISCOVERY;
@@ -2933,10 +2929,10 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
goto error_return;
/*
- * Hook any oauth_token into the PGconn immediately so that
- * the allocation isn't lost in case of an error.
+ * Hook any oauth_token into the request struct immediately so
+ * that the allocation isn't lost in case of an error.
*/
- set_conn_oauth_token(conn, oauth_token);
+ request->v1.token = oauth_token;
if (!actx->user_prompted)
{
@@ -2965,7 +2961,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
* the client wait directly on the timerfd rather than the
* multiplexer.
*/
- set_conn_altsock(conn, actx->timerfd);
+ *altsock = actx->timerfd;
actx->step = OAUTH_STEP_WAIT_INTERVAL;
actx->running = 1;
@@ -2991,48 +2987,21 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
return oauth_token ? PGRES_POLLING_OK : PGRES_POLLING_READING;
error_return:
- errbuf = conn_errorMessage(conn);
-
- /*
- * Assemble the three parts of our error: context, body, and detail. See
- * also the documentation for struct async_ctx.
- */
- if (actx->errctx)
- appendPQExpBuffer(errbuf, "%s: ", actx->errctx);
-
- if (PQExpBufferDataBroken(actx->errbuf))
- appendPQExpBufferStr(errbuf, libpq_gettext("out of memory"));
- else
- appendPQExpBufferStr(errbuf, actx->errbuf.data);
-
- if (actx->curl_err[0])
- {
- appendPQExpBuffer(errbuf, " (libcurl: %s)", actx->curl_err);
-
- /* Sometimes libcurl adds a newline to the error buffer. :( */
- if (errbuf->len >= 2 && errbuf->data[errbuf->len - 2] == '\n')
- {
- errbuf->data[errbuf->len - 2] = ')';
- errbuf->data[errbuf->len - 1] = '\0';
- errbuf->len--;
- }
- }
-
- appendPQExpBufferChar(errbuf, '\n');
+ append_actx_error(request, actx);
return PGRES_POLLING_FAILED;
}
/*
- * The top-level entry point. This is a convenient place to put necessary
- * wrapper logic before handing off to the true implementation, above.
+ * The top-level entry point for the flow. This is a convenient place to put
+ * necessary wrapper logic before handing off to the true implementation, above.
*/
-PostgresPollingStatusType
-pg_fe_run_oauth_flow(PGconn *conn)
+static PostgresPollingStatusType
+pg_fe_run_oauth_flow(PGconn *conn, struct PGoauthBearerRequest *request,
+ int *altsock)
{
PostgresPollingStatusType result;
- fe_oauth_state *state = conn_sasl_state(conn);
- struct async_ctx *actx;
+ struct async_ctx *actx = request->user;
#ifndef WIN32
sigset_t osigset;
bool sigpipe_pending;
@@ -3059,20 +3028,16 @@ pg_fe_run_oauth_flow(PGconn *conn)
masked = (pq_block_sigpipe(&osigset, &sigpipe_pending) == 0);
#endif
- result = pg_fe_run_oauth_flow_impl(conn);
+ result = pg_fe_run_oauth_flow_impl(conn,
+ (PGoauthBearerRequestV2 *) request,
+ altsock);
/*
* To assist with finding bugs in comb_multiplexer() and
* drain_timer_events(), when we're in debug mode, track the total number
* of calls to this function and print that at the end of the flow.
- *
- * Be careful that state->async_ctx could be NULL if early initialization
- * fails during the first call.
*/
- actx = state->async_ctx;
- Assert(actx || result == PGRES_POLLING_FAILED);
-
- if (actx && actx->debugging)
+ if (actx->debugging)
{
actx->dbg_num_calls++;
if (result == PGRES_POLLING_OK || result == PGRES_POLLING_FAILED)
@@ -3093,3 +3058,104 @@ pg_fe_run_oauth_flow(PGconn *conn)
return result;
}
+
+/*
+ * Callback registration for OAUTHBEARER. libpq calls this once per OAuth
+ * connection.
+ */
+int
+pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request)
+{
+ struct async_ctx *actx;
+ PQconninfoOption *conninfo = NULL;
+
+ if (!initialize_curl(request))
+ return -1;
+
+ /*
+ * Create our asynchronous state, and hook it into the upper-level OAuth
+ * state immediately, so any failures below won't leak the context
+ * allocation.
+ */
+ actx = calloc(1, sizeof(*actx));
+ if (!actx)
+ goto oom;
+
+ actx->mux = PGINVALID_SOCKET;
+ actx->timerfd = -1;
+
+ /*
+ * Now we have a valid (but still useless) actx, so we can fill in the
+ * request object. From this point onward, failures will result in a call
+ * to pg_fe_cleanup_oauth_flow(). Further cleanup logic belongs there.
+ */
+ request->v1.async = pg_fe_run_oauth_flow;
+ request->v1.cleanup = pg_fe_cleanup_oauth_flow;
+ request->v1.user = actx;
+
+ /*
+ * Now finish filling in the actx.
+ */
+
+ /* Should we enable unsafe features? */
+ actx->debugging = oauth_unsafe_debugging_enabled();
+
+ initPQExpBuffer(&actx->work_data);
+ initPQExpBuffer(&actx->errbuf);
+
+ /* Pull relevant connection options. */
+ conninfo = PQconninfo(conn);
+ if (!conninfo)
+ goto oom;
+
+ for (PQconninfoOption *opt = conninfo; opt->keyword; opt++)
+ {
+ if (!opt->val)
+ continue; /* simplifies the strdup logic below */
+
+ if (strcmp(opt->keyword, "oauth_client_id") == 0)
+ {
+ actx->client_id = strdup(opt->val);
+ if (!actx->client_id)
+ goto oom;
+ }
+ else if (strcmp(opt->keyword, "oauth_client_secret") == 0)
+ {
+ actx->client_secret = strdup(opt->val);
+ if (!actx->client_secret)
+ goto oom;
+ }
+ }
+
+ PQconninfoFree(conninfo);
+ conninfo = NULL; /* keeps `goto oom` safe */
+
+ actx->discovery_uri = request->v1.openid_configuration;
+ actx->issuer_id = request->issuer;
+ actx->scope = request->v1.scope;
+
+ Assert(actx->client_id); /* ensured by setup_oauth_parameters() */
+ Assert(actx->issuer_id); /* ensured by setup_oauth_parameters() */
+ Assert(actx->discovery_uri); /* ensured by oauth_exchange() */
+
+ if (!setup_multiplexer(actx))
+ {
+ append_actx_error(request, actx);
+ return -1;
+ }
+
+ if (!setup_curl_handles(actx))
+ {
+ append_actx_error(request, actx);
+ return -1;
+ }
+
+ return 0;
+
+oom:
+ if (conninfo)
+ PQconninfoFree(conninfo);
+
+ request->error = libpq_gettext("out of memory");
+ return -1;
+}
diff --git a/src/interfaces/libpq-oauth/oauth-utils.c b/src/interfaces/libpq-oauth/oauth-utils.c
index 4ebe7d0948c..ccb0d9bf2c5 100644
--- a/src/interfaces/libpq-oauth/oauth-utils.c
+++ b/src/interfaces/libpq-oauth/oauth-utils.c
@@ -35,85 +35,18 @@
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.
*
- * 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
- *
- * 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.
+ * The current implementation relies on libpq_gettext to translate error
+ * messages using libpq's message domain, so libpq injects it here. We also use
+ * this chance to initialize our threadlock.
*/
void
-libpq_oauth_init(pgthreadlock_t threadlock_impl,
- libpq_gettext_func gettext_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)
+libpq_oauth_init(libpq_gettext_func gettext_impl)
{
- pg_g_threadlock = threadlock_impl;
+ pg_g_threadlock = PQgetThreadLock();
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;
-}
-
-/*
- * 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
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index 2aef327c68b..f93184f04db 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -78,7 +78,7 @@ oauth_init(PGconn *conn, const char *password,
* This handles only mechanism state tied to the connection lifetime; state
* stored in state->async_ctx is freed up either immediately after the
* authentication handshake succeeds, or before the mechanism is cleaned up on
- * failure. See pg_fe_cleanup_oauth_flow() and cleanup_user_oauth_flow().
+ * failure. See pg_fe_cleanup_oauth_flow() and cleanup_oauth_flow().
*/
static void
oauth_free(void *opaq)
@@ -680,30 +680,54 @@ cleanup:
* it's added to conn->errorMessage here.
*/
static void
-report_user_flow_error(PGconn *conn, const PGoauthBearerRequestV2 *request)
+report_flow_error(PGconn *conn, const PGoauthBearerRequestV2 *request)
{
- appendPQExpBufferStr(&conn->errorMessage,
- libpq_gettext("user-defined OAuth flow failed"));
+ fe_oauth_state *state = conn->sasl_state;
+ const char *errmsg = request->error;
+
+ /*
+ * User-defined flows are called out explicitly so that the user knows who
+ * to blame. Builtin flows don't need that extra message length; we expect
+ * them to always fill in request->error on failure anyway.
+ */
+ if (state->builtin)
+ {
+ if (!errmsg)
+ {
+ /*
+ * Don't turn a bug here into a crash in production, but don't
+ * bother translating either.
+ */
+ Assert(false);
+ errmsg = "builtin flow failed but did not provide an error message";
+ }
- if (request->error)
+ appendPQExpBufferStr(&conn->errorMessage, errmsg);
+ }
+ else
{
- appendPQExpBufferStr(&conn->errorMessage, ": ");
- appendPQExpBufferStr(&conn->errorMessage, request->error);
+ appendPQExpBufferStr(&conn->errorMessage,
+ libpq_gettext("user-defined OAuth flow failed"));
+ if (errmsg)
+ {
+ appendPQExpBufferStr(&conn->errorMessage, ": ");
+ appendPQExpBufferStr(&conn->errorMessage, errmsg);
+ }
}
appendPQExpBufferChar(&conn->errorMessage, '\n');
}
/*
- * Callback implementation of conn->async_auth() for a user-defined OAuth flow.
- * Delegates the retrieval of the token to the application's async callback.
+ * Callback implementation of conn->async_auth() for OAuth flows. Delegates the
+ * retrieval of the token to the PGoauthBearerRequestV2.async() callback.
*
- * This will be called multiple times as needed; the application is responsible
- * for setting an altsock to signal and returning the correct PGRES_POLLING_*
+ * This will be called multiple times as needed; the callback is responsible for
+ * setting an altsock to signal and returning the correct PGRES_POLLING_*
* statuses for use by PQconnectPoll().
*/
static PostgresPollingStatusType
-run_user_oauth_flow(PGconn *conn)
+run_oauth_flow(PGconn *conn)
{
fe_oauth_state *state = conn->sasl_state;
PGoauthBearerRequestV2 *request = state->async_ctx;
@@ -711,6 +735,7 @@ run_user_oauth_flow(PGconn *conn)
if (!request->v1.async)
{
+ Assert(!state->builtin); /* be very noisy if our code does this */
libpq_append_conn_error(conn,
"user-defined OAuth flow provided neither a token nor an async callback");
return PGRES_POLLING_FAILED;
@@ -722,7 +747,7 @@ run_user_oauth_flow(PGconn *conn)
if (status == PGRES_POLLING_FAILED)
{
- report_user_flow_error(conn, request);
+ report_flow_error(conn, request);
return status;
}
else if (status == PGRES_POLLING_OK)
@@ -734,6 +759,7 @@ run_user_oauth_flow(PGconn *conn)
*/
if (!request->v1.token)
{
+ Assert(!state->builtin);
libpq_append_conn_error(conn,
"user-defined OAuth flow did not provide a token");
return PGRES_POLLING_FAILED;
@@ -752,6 +778,7 @@ run_user_oauth_flow(PGconn *conn)
/* The hook wants the client to poll the altsock. Make sure it set one. */
if (conn->altsock == PGINVALID_SOCKET)
{
+ Assert(!state->builtin);
libpq_append_conn_error(conn,
"user-defined OAuth flow did not provide a socket for polling");
return PGRES_POLLING_FAILED;
@@ -761,12 +788,16 @@ run_user_oauth_flow(PGconn *conn)
}
/*
- * Cleanup callback for the async user flow. Delegates most of its job to
+ * Cleanup callback for the async flow. Delegates most of its job to
* PGoauthBearerRequest.cleanup(), then disconnects the altsock and frees the
* request itself.
+ *
+ * This is called either at the end of a successful authentication, or during
+ * pqDropConnection(), so we won't leak resources even if PQconnectPoll() never
+ * calls us back.
*/
static void
-cleanup_user_oauth_flow(PGconn *conn)
+cleanup_oauth_flow(PGconn *conn)
{
fe_oauth_state *state = conn->sasl_state;
PGoauthBearerRequestV2 *request = state->async_ctx;
@@ -786,12 +817,16 @@ cleanup_user_oauth_flow(PGconn *conn)
*
* There are three potential implementations of use_builtin_flow:
*
- * 1) If the OAuth client is disabled at configuration time, return false.
+ * 1) If the OAuth client is disabled at configuration time, return zero.
* 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.
+ *
+ * For caller convenience, the return value follows the convention of
+ * PQauthDataHook: zero means no implementation is provided, negative indicates
+ * failure, and positive indicates success.
*/
#if !defined(USE_LIBCURL)
@@ -800,10 +835,10 @@ cleanup_user_oauth_flow(PGconn *conn)
* This configuration doesn't support the builtin flow.
*/
-bool
-use_builtin_flow(PGconn *conn, fe_oauth_state *state)
+static int
+use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *request)
{
- return false;
+ return 0;
}
#elif defined(USE_DYNAMIC_OAUTH)
@@ -814,36 +849,6 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state)
typedef char *(*libpq_gettext_func) (const char *msgid);
-/*
- * 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.)
- */
-
-#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
* callbacks into the connection's async auth handlers.
@@ -852,27 +857,19 @@ DEFINE_SETTER(char *, oauth_token);
* 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.
+ *
+ * The lifetime of *request ends shortly after this call, so it must be copied
+ * to longer-lived storage.
*/
-bool
-use_builtin_flow(PGconn *conn, fe_oauth_state *state)
+static int
+use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *request)
{
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,
- 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);
+ void (*init) (libpq_gettext_func gettext_impl);
+ int (*start_flow) (PGconn *conn, PGoauthBearerRequestV2 *request);
/*
* On macOS only, load the module using its absolute install path; the
@@ -885,9 +882,9 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state)
*/
const char *const module_name =
#if defined(__darwin__)
- LIBDIR "/libpq-oauth-" PG_MAJORVERSION DLSUFFIX;
+ LIBDIR "/libpq-oauth" DLSUFFIX;
#else
- "libpq-oauth-" PG_MAJORVERSION DLSUFFIX;
+ "libpq-oauth" DLSUFFIX;
#endif
state->builtin_flow = dlopen(module_name, RTLD_NOW | RTLD_LOCAL);
@@ -903,22 +900,25 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state)
if (oauth_unsafe_debugging_enabled())
fprintf(stderr, "failed dlopen for libpq-oauth: %s\n", dlerror());
- return false;
+ return 0;
}
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)
+ || (start_flow = dlsym(state->builtin_flow, "pg_start_oauthbearer")) == NULL)
{
/*
- * This is more of an error condition than the one above, but due to
- * the dlerror() threadsafety issue, lock it behind PGOAUTHDEBUG too.
+ * This is more of an error condition than the one above, but the
+ * cause is still locked behind PGOAUTHDEBUG due to the dlerror()
+ * threadsafety issue.
*/
if (oauth_unsafe_debugging_enabled())
fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
dlclose(state->builtin_flow);
- return false;
+ state->builtin_flow = NULL;
+
+ request->error = libpq_gettext("could not find entry point for libpq-oauth");
+ return -1;
}
/*
@@ -937,57 +937,45 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state)
/* 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;
+ appendPQExpBuffer(&conn->errorMessage,
+ "use_builtin_flow: failed to lock mutex (%d)\n",
+ lockerr);
+
+ request->error = ""; /* satisfy report_flow_error() */
+ return -1;
}
if (!initialized)
{
- init(pg_g_threadlock,
+ init(
#ifdef ENABLE_NLS
- libpq_gettext,
+ libpq_gettext
#else
- NULL,
+ NULL
#endif
- 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;
}
pthread_mutex_unlock(&init_mutex);
- /* Set our asynchronous callbacks. */
- conn->async_auth = flow;
- conn->cleanup_async_auth = cleanup;
-
- return true;
+ return (start_flow(conn, request) == 0) ? 1 : -1;
}
#else
/*
- * Use the builtin flow in libpq-oauth.a (see libpq-oauth/oauth-curl.h).
+ * For static builds, we can just call pg_start_oauthbearer() directly. It's
+ * provided by libpq-oauth.a.
*/
-extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn);
-extern void pg_fe_cleanup_oauth_flow(PGconn *conn);
+extern int pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request);
-bool
-use_builtin_flow(PGconn *conn, fe_oauth_state *state)
+static int
+use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *request)
{
- /* Set our asynchronous callbacks. */
- conn->async_auth = pg_fe_run_oauth_flow;
- conn->cleanup_async_auth = pg_fe_cleanup_oauth_flow;
-
- return true;
+ return (pg_start_oauthbearer(conn, request) == 0) ? 1 : -1;
}
#endif /* USE_LIBCURL */
@@ -1000,11 +988,11 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state)
* If the application has registered a custom flow handler using
* PQAUTHDATA_OAUTH_BEARER_TOKEN[_V2], it may either return a token immediately
* (e.g. if it has one cached for immediate use), or set up for a series of
- * asynchronous callbacks which will be managed by run_user_oauth_flow().
+ * asynchronous callbacks which will be managed by run_oauth_flow().
*
* If the default handler is used instead, a Device Authorization flow is used
- * for the connection if support has been compiled in. (See
- * fe-auth-oauth-curl.c for implementation details.)
+ * for the connection if support has been compiled in. (See oauth-curl.c for
+ * implementation details.)
*
* If neither a custom handler nor the builtin flow is available, the connection
* fails here.
@@ -1026,11 +1014,17 @@ setup_token_request(PGconn *conn, fe_oauth_state *state)
/*
* The client may have overridden the OAuth flow. Try the v2 hook first,
- * then fall back to the v1 implementation.
+ * then fall back to the v1 implementation. If neither is available, try
+ * the builtin flow.
*/
res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN_V2, conn, &request);
if (res == 0)
res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN, conn, &request);
+ if (res == 0)
+ {
+ state->builtin = true;
+ res = use_builtin_flow(conn, state, &request);
+ }
if (res > 0)
{
@@ -1065,22 +1059,21 @@ setup_token_request(PGconn *conn, fe_oauth_state *state)
*request_copy = request;
- conn->async_auth = run_user_oauth_flow;
- conn->cleanup_async_auth = cleanup_user_oauth_flow;
+ conn->async_auth = run_oauth_flow;
+ conn->cleanup_async_auth = cleanup_oauth_flow;
state->async_ctx = request_copy;
- }
- else if (res < 0)
- {
- report_user_flow_error(conn, &request);
- goto fail;
- }
- else if (!use_builtin_flow(conn, state))
- {
- libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)");
- goto fail;
+
+ return true;
}
- return true;
+ /*
+ * Failure cases: either we tried to set up a flow and failed, or there
+ * was no flow to try.
+ */
+ if (res < 0)
+ report_flow_error(conn, &request);
+ else
+ libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)");
fail:
if (request.v1.cleanup)
--
2.34.1
[application/octet-stream] v7-0002-libpq-oauth-Never-link-against-libpq-s-encoding-f.patch (3.2K, 4-v7-0002-libpq-oauth-Never-link-against-libpq-s-encoding-f.patch)
download | inline diff:
From f59d6431b04f995f37184251e5680234caaf7c91 Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email protected]>
Date: Wed, 3 Dec 2025 09:53:44 -0800
Subject: [PATCH v7 2/5] libpq-oauth: Never link against libpq's encoding
functions
Now that libpq-oauth doesn't have to match the major version of libpq,
some things in pg_wchar.h are technically unsafe for us to use. (See
b6c7cfac8 for a fuller discussion.) This is unlikely to be a problem --
we only care about UTF-8 in the context of OAuth right now -- but if
anyone did introduce a way to hit it, it'd be extremely difficult to
debug or reproduce, and it'd be a potential security vulnerability to
boot.
Define USE_PRIVATE_ENCODING_FUNCS so that anyone who tries to add a
dependency on the exported APIs will simply fail to link the shared
module.
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Discussion: https://postgr.es/m/CAOYmi%2BmrGg%2Bn_X2MOLgeWcj3v_M00gR8uz_D7mM8z%3DdX1JYVbg%40mail.gmail.com
---
src/interfaces/libpq-oauth/meson.build | 10 +++++++++-
src/interfaces/libpq-oauth/Makefile | 11 +++++++++--
2 files changed, 18 insertions(+), 3 deletions(-)
diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build
index 685a00acf7a..ea3a900f4f1 100644
--- a/src/interfaces/libpq-oauth/meson.build
+++ b/src/interfaces/libpq-oauth/meson.build
@@ -12,7 +12,15 @@ libpq_oauth_sources = files(
libpq_oauth_so_sources = files(
'oauth-utils.c',
)
-libpq_oauth_so_c_args = ['-DUSE_DYNAMIC_OAUTH']
+libpq_oauth_so_c_args = [
+ '-DUSE_DYNAMIC_OAUTH',
+
+ # A bit of forward-looking paranoia: don't allow anyone to accidentally depend
+ # on the encoding IDs coming from libpq. They're not guaranteed to match the
+ # IDs in use by our version of pgcommon, now that we allow the major version
+ # of libpq to differ from the major version of libpq-oauth.
+ '-DUSE_PRIVATE_ENCODING_FUNCS',
+]
export_file = custom_target('libpq-oauth.exports',
kwargs: gen_export_kwargs,
diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile
index e90482566b1..231349034d1 100644
--- a/src/interfaces/libpq-oauth/Makefile
+++ b/src/interfaces/libpq-oauth/Makefile
@@ -24,6 +24,14 @@ override shlib := lib$(NAME)$(DLSUFFIX)
override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port $(CPPFLAGS) $(LIBCURL_CPPFLAGS)
override CFLAGS += $(PTHREAD_CFLAGS)
+override CPPFLAGS_SHLIB := -DUSE_DYNAMIC_OAUTH
+
+# A bit of forward-looking paranoia: don't allow libpq-oauth.so to accidentally
+# depend on the encoding IDs coming from libpq. They're not guaranteed to match
+# the IDs in use by our version of pgcommon, now that we allow the major version
+# of libpq to differ from the major version of libpq-oauth.
+override CPPFLAGS_SHLIB += -DUSE_PRIVATE_ENCODING_FUNCS
+
OBJS = \
$(WIN32RES)
@@ -34,8 +42,7 @@ 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
+oauth-utils.o: override CPPFLAGS += $(CPPFLAGS_SHLIB)
# Add shlib-/stlib-specific objects.
$(shlib): override OBJS += $(OBJS_SHLIB)
--
2.34.1
[application/octet-stream] v7-0003-libpq-Poison-the-v2-part-of-a-v1-Bearer-request.patch (7.0K, 5-v7-0003-libpq-Poison-the-v2-part-of-a-v1-Bearer-request.patch)
download | inline diff:
From 75ac7ddc1202849aa144f25ce3db587002bb2234 Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email protected]>
Date: Tue, 3 Mar 2026 09:45:37 -0800
Subject: [PATCH v7 3/5] libpq: Poison the v2 part of a v1 Bearer request
The new PGoauthBearerRequestV2 API (which has similarities to the
"subclass" pointer architecture in use by the backend, for Nodes)
carries the risk of a developer ignoring the type of hook in use and
just casting directly to the V2 struct. This will appear to work fine in
19, but crash (or worse) when speaking to libpq 18.
However, we're in a unique position to catch this problem, because we
have tight control over the struct. Add poisoning code to the v1 path
which does the following:
- masks the v2 request->issuer pointer, to hopefully point at nonsense
memory
- abort()s if the v2 request->error is assigned by the hook
- attempts to cover both with VALGRIND_MAKE_MEM_NOACCESS for the
duration of the callback (a potential AddressSanitizer implementation
is left for future work)
The struct is unpoisoned after the call, so we can switch back to the v2
internal implementation when necessary.
Discussion: https://postgr.es/m/CAOYmi%2BmrGg%2Bn_X2MOLgeWcj3v_M00gR8uz_D7mM8z%3DdX1JYVbg%40mail.gmail.com
---
src/interfaces/libpq/fe-auth-oauth.h | 1 +
src/interfaces/libpq/fe-auth-oauth.c | 133 +++++++++++++++++++++++++--
2 files changed, 125 insertions(+), 9 deletions(-)
diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h
index 511284614f7..2b22d23ffde 100644
--- a/src/interfaces/libpq/fe-auth-oauth.h
+++ b/src/interfaces/libpq/fe-auth-oauth.h
@@ -34,6 +34,7 @@ typedef struct
PGconn *conn;
void *async_ctx;
+ bool v1;
bool builtin;
void *builtin_flow;
} fe_oauth_state;
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index f93184f04db..40800183eb9 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -27,6 +27,11 @@
#include "fe-auth-oauth.h"
#include "mb/pg_wchar.h"
#include "pg_config_paths.h"
+#include "utils/memdebug.h"
+
+static int do_async(fe_oauth_state *state, PGoauthBearerRequestV2 *request);
+static void do_cleanup(fe_oauth_state *state, PGoauthBearerRequestV2 *request);
+static void poison_req_v2(PGoauthBearerRequestV2 *request, bool poison);
/* The exported OAuth callback mechanism. */
static void *oauth_init(PGconn *conn, const char *password,
@@ -741,9 +746,7 @@ run_oauth_flow(PGconn *conn)
return PGRES_POLLING_FAILED;
}
- status = request->v1.async(conn,
- (PGoauthBearerRequest *) request,
- &conn->altsock);
+ status = do_async(state, request);
if (status == PGRES_POLLING_FAILED)
{
@@ -804,8 +807,7 @@ cleanup_oauth_flow(PGconn *conn)
Assert(request);
- if (request->v1.cleanup)
- request->v1.cleanup(conn, (PGoauthBearerRequest *) request);
+ do_cleanup(state, request);
conn->altsock = PGINVALID_SOCKET;
free(request);
@@ -1019,7 +1021,14 @@ setup_token_request(PGconn *conn, fe_oauth_state *state)
*/
res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN_V2, conn, &request);
if (res == 0)
+ {
+ poison_req_v2(&request, true);
+
res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN, conn, &request);
+ state->v1 = (res != 0);
+
+ poison_req_v2(&request, false);
+ }
if (res == 0)
{
state->builtin = true;
@@ -1045,8 +1054,7 @@ setup_token_request(PGconn *conn, fe_oauth_state *state)
}
/* short-circuit */
- if (request.v1.cleanup)
- request.v1.cleanup(conn, (PGoauthBearerRequest *) &request);
+ do_cleanup(state, &request);
return true;
}
@@ -1076,8 +1084,7 @@ setup_token_request(PGconn *conn, fe_oauth_state *state)
libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)");
fail:
- if (request.v1.cleanup)
- request.v1.cleanup(conn, (PGoauthBearerRequest *) &request);
+ do_cleanup(state, &request);
return false;
}
@@ -1428,3 +1435,111 @@ oauth_unsafe_debugging_enabled(void)
return (env && strcmp(env, "UNSAFE") == 0);
}
+
+/*
+ * Hook v1 Poisoning
+ *
+ * Try to catch misuses of the v1 PQAUTHDATA_OAUTH_BEARER_TOKEN hook and its
+ * callbacks, which are not allowed to downcast their request argument to
+ * PGoauthBearerRequestV2. (Such clients may crash or worse when speaking to
+ * libpq 18.)
+ *
+ * This attempts to use Valgrind hooks, if present, to mark the extra members as
+ * inaccessible. For uninstrumented builds, it also munges request->issuer to
+ * try to crash clients that perform string operations, and it aborts if
+ * request->error is set.
+ */
+
+#define MASK_BITS ((uintptr_t) 0x55aa55aa55aa55aa)
+#define POISON_MASK(ptr) ((void *) (((uintptr_t) ptr) ^ MASK_BITS))
+
+/*
+ * Workhorse for v2 request poisoning. This must be called exactly twice: once
+ * to poison, once to unpoison.
+ *
+ * NB: Unpoisoning must restore the request to its original state, because we
+ * might still switch back to a v2 implementation internally. Don't do anything
+ * destructive during the poison operation.
+ */
+static void
+poison_req_v2(PGoauthBearerRequestV2 *request, bool poison)
+{
+ void *const base = (char *) request + sizeof(request->v1);
+ const size_t len = sizeof(*request) - sizeof(request->v1);
+
+ if (poison)
+ {
+ /* Poison request->issuer with a mask to help uninstrumented builds. */
+ request->issuer = POISON_MASK(request->issuer);
+
+ VALGRIND_MAKE_MEM_NOACCESS(base, len);
+ }
+ else
+ {
+ /*
+ * XXX Using DEFINED here is technically too lax; we might catch
+ * struct padding in the blast radius. But since this API has to
+ * poison stack addresses, and Valgrind can't track/manage undefined
+ * stack regions, we can't be any stricter without tracking the
+ * original state of the memory.
+ */
+ VALGRIND_MAKE_MEM_DEFINED(base, len);
+
+ /* Undo our mask. */
+ request->issuer = POISON_MASK(request->issuer);
+
+ /*
+ * For uninstrumented builds, make sure request->error wasn't touched.
+ */
+ if (request->error)
+ {
+ fprintf(stderr,
+ "abort! out-of-bounds write to PGoauthBearerRequest by PQAUTHDATA_OAUTH_BEARER_TOKEN hook\n");
+ abort();
+ }
+ }
+}
+
+/*
+ * Wrapper around PGoauthBearerRequest.async() which applies poison during the
+ * callback when necessary.
+ */
+static int
+do_async(fe_oauth_state *state, PGoauthBearerRequestV2 *request)
+{
+ PGconn *conn = state->conn;
+ int ret;
+
+ Assert(request->v1.async);
+
+ if (state->v1)
+ poison_req_v2(request, true);
+
+ ret = request->v1.async(conn,
+ (PGoauthBearerRequest *) request,
+ &conn->altsock);
+
+ if (state->v1)
+ poison_req_v2(request, false);
+
+ return ret;
+}
+
+/*
+ * Similar wrapper for the optional PGoauthBearerRequest.cleanup() callback.
+ * Does nothing if one is not defined.
+ */
+static void
+do_cleanup(fe_oauth_state *state, PGoauthBearerRequestV2 *request)
+{
+ if (!request->v1.cleanup)
+ return;
+
+ if (state->v1)
+ poison_req_v2(request, true);
+
+ request->v1.cleanup(state->conn, (PGoauthBearerRequest *) request);
+
+ if (state->v1)
+ poison_req_v2(request, false);
+}
--
2.34.1
[application/octet-stream] v7-0004-libpq-Allow-developers-to-reimplement-libpq-oauth.patch (4.8K, 6-v7-0004-libpq-Allow-developers-to-reimplement-libpq-oauth.patch)
download | inline diff:
From 7a43398c90ac9ed2471879762dc660a9f23d700d Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email protected]>
Date: Fri, 6 Mar 2026 14:18:49 -0800
Subject: [PATCH v7 4/5] libpq: Allow developers to reimplement libpq-oauth
For PG19, since we won't have the ability to officially switch out flow
plugins, relax the flow-loading code to not require the internal init
function. Modules that don't have one will be treated as custom user
flows in error messages.
This will let bleeding-edge developers more easily test out the API and
provide feedback for PG20, by telling the runtime linker to find a
different libpq-oauth. It remains undocumented for end users.
Discussion: https://postgr.es/m/CAOYmi%2BmrGg%2Bn_X2MOLgeWcj3v_M00gR8uz_D7mM8z%3DdX1JYVbg%40mail.gmail.com
---
src/interfaces/libpq/fe-auth-oauth.h | 2 +-
src/interfaces/libpq/fe-auth-oauth.c | 71 ++++++++++++++++------------
2 files changed, 42 insertions(+), 31 deletions(-)
diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h
index 2b22d23ffde..872f5df551a 100644
--- a/src/interfaces/libpq/fe-auth-oauth.h
+++ b/src/interfaces/libpq/fe-auth-oauth.h
@@ -36,7 +36,7 @@ typedef struct
bool v1;
bool builtin;
- void *builtin_flow;
+ void *flow_module;
} fe_oauth_state;
extern void pqClearOAuthToken(PGconn *conn);
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index 40800183eb9..dff00dfc6c9 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -889,8 +889,8 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
"libpq-oauth" DLSUFFIX;
#endif
- state->builtin_flow = dlopen(module_name, RTLD_NOW | RTLD_LOCAL);
- if (!state->builtin_flow)
+ state->flow_module = dlopen(module_name, RTLD_NOW | RTLD_LOCAL);
+ if (!state->flow_module)
{
/*
* For end users, this probably isn't an error condition, it just
@@ -905,8 +905,16 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
return 0;
}
- if ((init = dlsym(state->builtin_flow, "libpq_oauth_init")) == NULL
- || (start_flow = dlsym(state->builtin_flow, "pg_start_oauthbearer")) == NULL)
+ /*
+ * Our libpq-oauth.so provides a special initialization function for libpq
+ * integration. If we don't find this, assume that a custom module is in
+ * use instead.
+ */
+ init = dlsym(state->flow_module, "libpq_oauth_init");
+ if (!init)
+ state->builtin = false; /* adjust our error messages */
+
+ if ((start_flow = dlsym(state->flow_module, "pg_start_oauthbearer")) == NULL)
{
/*
* This is more of an error condition than the one above, but the
@@ -916,8 +924,8 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
if (oauth_unsafe_debugging_enabled())
fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
- dlclose(state->builtin_flow);
- state->builtin_flow = NULL;
+ dlclose(state->flow_module);
+ state->flow_module = NULL;
request->error = libpq_gettext("could not find entry point for libpq-oauth");
return -1;
@@ -928,39 +936,42 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
* 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)
+ if (init)
{
- /* Should not happen... but don't continue if it does. */
- Assert(false);
+ /*
+ * 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);
- appendPQExpBuffer(&conn->errorMessage,
- "use_builtin_flow: failed to lock mutex (%d)\n",
- lockerr);
+ appendPQExpBuffer(&conn->errorMessage,
+ "use_builtin_flow: failed to lock mutex (%d)\n",
+ lockerr);
- request->error = ""; /* satisfy report_flow_error() */
- return -1;
- }
+ request->error = ""; /* satisfy report_flow_error() */
+ return -1;
+ }
- if (!initialized)
- {
- init(
+ if (!initialized)
+ {
+ init(
#ifdef ENABLE_NLS
- libpq_gettext
+ libpq_gettext
#else
- NULL
+ NULL
#endif
- );
+ );
- initialized = true;
- }
+ initialized = true;
+ }
- pthread_mutex_unlock(&init_mutex);
+ pthread_mutex_unlock(&init_mutex);
+ }
return (start_flow(conn, request) == 0) ? 1 : -1;
}
--
2.34.1
[application/octet-stream] v7-0005-WIP-Introduce-third-party-OAuth-flow-plugins.patch (36.1K, 7-v7-0005-WIP-Introduce-third-party-OAuth-flow-plugins.patch)
download | inline diff:
From b3daf3140f8f957edf48f82da182a42eca48ec25 Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email protected]>
Date: Wed, 3 Dec 2025 15:47:23 -0800
Subject: [PATCH v7 5/5] WIP: Introduce third-party OAuth flow plugins?
[Not intended for PG19.]
This experimental commit promotes the pg_start_oauthbearer API to a
public header (libpq-oauth.h) and adds a PGOAUTHMODULE environment
variable that overrides the load path for the plugin, allowing users to
provide their own. It's probably not a good solution; it provides almost
nothing over LD_LIBRARY_PATH (other than not being LD_LIBRARY_PATH).
This is a relatively small amount of implementation change, but
unfortunately the tests have a large amount of code motion to be able to
share logic between the test executable and plugin. I might need to
split that into multiple squash! commits to make it more easily
reviewable.
TODO: figure out PGDLLEXPORT, which we do not currently provide publicly
TODO: lock down PGOAUTHMODULE as necessary to avoid introducing exciting
new vulnerabilities
TODO: how hard would it be to support Windows here?
Reviewed-by: Zsolt Parragi <[email protected]>
Discussion: https://postgr.es/m/CAOYmi%2BmrGg%2Bn_X2MOLgeWcj3v_M00gR8uz_D7mM8z%3DdX1JYVbg%40mail.gmail.com
---
src/interfaces/libpq/meson.build | 1 +
src/interfaces/libpq/Makefile | 2 +
src/interfaces/libpq-oauth/oauth-curl.h | 24 --
src/interfaces/libpq/libpq-oauth.h | 52 +++
src/interfaces/libpq-oauth/oauth-curl.c | 2 +-
src/interfaces/libpq/fe-auth-oauth.c | 46 ++-
src/test/modules/oauth_validator/meson.build | 15 +
src/test/modules/oauth_validator/Makefile | 12 +-
.../oauth_validator/oauth_test_common.h | 26 ++
src/test/modules/oauth_validator/oauth_flow.c | 72 ++++
.../oauth_validator/oauth_hook_client.c | 318 +--------------
.../oauth_validator/oauth_test_common.c | 373 ++++++++++++++++++
.../modules/oauth_validator/t/002_client.pl | 57 ++-
13 files changed, 646 insertions(+), 354 deletions(-)
delete mode 100644 src/interfaces/libpq-oauth/oauth-curl.h
create mode 100644 src/interfaces/libpq/libpq-oauth.h
create mode 100644 src/test/modules/oauth_validator/oauth_test_common.h
create mode 100644 src/test/modules/oauth_validator/oauth_flow.c
create mode 100644 src/test/modules/oauth_validator/oauth_test_common.c
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index b0ae72167a1..4eb029cc2a6 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -135,6 +135,7 @@ pkgconfig.generate(
install_headers(
'libpq-fe.h',
'libpq-events.h',
+ 'libpq-oauth.h',
)
install_headers(
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index 0963995eed4..a65c36a63fd 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -157,6 +157,7 @@ $(top_builddir)/src/port/pg_config_paths.h:
install: all installdirs install-lib
$(INSTALL_DATA) $(srcdir)/libpq-fe.h '$(DESTDIR)$(includedir)'
$(INSTALL_DATA) $(srcdir)/libpq-events.h '$(DESTDIR)$(includedir)'
+ $(INSTALL_DATA) $(srcdir)/libpq-oauth.h '$(DESTDIR)$(includedir)'
$(INSTALL_DATA) $(srcdir)/libpq-int.h '$(DESTDIR)$(includedir_internal)'
$(INSTALL_DATA) $(srcdir)/fe-auth-sasl.h '$(DESTDIR)$(includedir_internal)'
$(INSTALL_DATA) $(srcdir)/pqexpbuffer.h '$(DESTDIR)$(includedir_internal)'
@@ -179,6 +180,7 @@ installdirs: installdirs-lib
uninstall: uninstall-lib
rm -f '$(DESTDIR)$(includedir)/libpq-fe.h'
rm -f '$(DESTDIR)$(includedir)/libpq-events.h'
+ rm -f '$(DESTDIR)$(includedir)/libpq-oauth.h'
rm -f '$(DESTDIR)$(includedir_internal)/libpq-int.h'
rm -f '$(DESTDIR)$(includedir_internal)/fe-auth-sasl.h'
rm -f '$(DESTDIR)$(includedir_internal)/pqexpbuffer.h'
diff --git a/src/interfaces/libpq-oauth/oauth-curl.h b/src/interfaces/libpq-oauth/oauth-curl.h
deleted file mode 100644
index 1d4dd766217..00000000000
--- a/src/interfaces/libpq-oauth/oauth-curl.h
+++ /dev/null
@@ -1,24 +0,0 @@
-/*-------------------------------------------------------------------------
- *
- * oauth-curl.h
- *
- * Definitions for OAuth Device Authorization module
- *
- * Portions Copyright (c) 1996-2026, 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 flow callback. */
-extern PGDLLEXPORT int pg_start_oauthbearer(PGconn *conn,
- PGoauthBearerRequestV2 *request);
-
-#endif /* OAUTH_CURL_H */
diff --git a/src/interfaces/libpq/libpq-oauth.h b/src/interfaces/libpq/libpq-oauth.h
new file mode 100644
index 00000000000..2a62b330b1c
--- /dev/null
+++ b/src/interfaces/libpq/libpq-oauth.h
@@ -0,0 +1,52 @@
+/*-------------------------------------------------------------------------
+ *
+ * libpq-oauth.h
+ * This file contains structs and functions used by custom OAuth plugins.
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ * src/interfaces/libpq/libpq-oauth.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef LIBPQ_OAUTH_H
+#define LIBPQ_OAUTH_H
+
+#include "libpq-fe.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+/* XXX can't rely on c.h, but duplicating this is asking for trouble */
+#ifndef PGDLLEXPORT
+#ifdef _WIN32
+#define PGDLLEXPORT __declspec (dllexport)
+#elif defined(__has_attribute)
+#if __has_attribute(visibility)
+#define PGDLLEXPORT __attribute__((visibility("default")))
+#else
+#define PGDLLEXPORT
+#endif
+#else
+#define PGDLLEXPORT
+#endif
+#endif
+
+/*
+ * V1 API
+ *
+ * Flow plugins must provide an implementation of this callback.
+ *
+ * TODO: provide a magic struct that allows backwards but not forwards compat?
+ */
+extern PGDLLEXPORT int pg_start_oauthbearer(PGconn *conn,
+ PGoauthBearerRequestV2 *request);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* LIBPQ_OAUTH_H */
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 052ecd32da2..400ca07c297 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -29,8 +29,8 @@
#endif
#include "common/jsonapi.h"
+#include "libpq-oauth.h"
#include "mb/pg_wchar.h"
-#include "oauth-curl.h"
#ifdef USE_DYNAMIC_OAUTH
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index dff00dfc6c9..d637853d66f 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -17,6 +17,8 @@
#ifdef USE_DYNAMIC_OAUTH
#include <dlfcn.h>
+#else
+#include "libpq-oauth.h"
#endif
#include "common/base64.h"
@@ -882,33 +884,55 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
* 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 =
+ const char *module_name =
#if defined(__darwin__)
LIBDIR "/libpq-oauth" DLSUFFIX;
#else
"libpq-oauth" DLSUFFIX;
#endif
+ /*-
+ * Additionally, the user may override the module path explicitly to be
+ * able to provide their own module, via PGOAUTHMODULE.
+ *
+ * TODO: have to think about _all_ the security ramifications of this. What
+ * existing protections in LD_LIBRARY_PATH (and/or SIP) are we potentially
+ * bypassing? Should we check the permissions of the file somehow...?
+ * TODO: maybe disallow anything not underneath LIBDIR? or PKGLIBDIR?
+ * Should it have a naming convention?
+ */
+ const char *env = getenv("PGOAUTHMODULE");
+
+ if (env && env[0])
+ module_name = env;
+ else
+ state->builtin = true;
+
state->flow_module = dlopen(module_name, RTLD_NOW | RTLD_LOCAL);
if (!state->flow_module)
{
/*
* 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.
+ * may want to debug this via the PGOAUTHDEBUG envvar, though, and we
+ * should be more noisy if users tried to provide a PGOAUTHMODULE.
*
* 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());
+ fprintf(stderr, "failed dlopen for %s: %s\n", module_name, dlerror());
+
+ if (state->builtin)
+ return 0;
- return 0;
+ request->error = libpq_gettext("plugin could not be loaded");
+ return -1;
}
/*
* Our libpq-oauth.so provides a special initialization function for libpq
- * integration. If we don't find this, assume that a custom module is in
- * use instead.
+ * integration. It's not a problem if we don't find this; it just means
+ * that a user-defined module is being used.
*/
init = dlsym(state->flow_module, "libpq_oauth_init");
if (!init)
@@ -922,12 +946,14 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
* threadsafety issue.
*/
if (oauth_unsafe_debugging_enabled())
- fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
+ fprintf(stderr, "failed dlsym for %s: %s\n", module_name, dlerror());
dlclose(state->flow_module);
state->flow_module = NULL;
- request->error = libpq_gettext("could not find entry point for libpq-oauth");
+ request->error = state->builtin ?
+ libpq_gettext("could not find entry point for libpq-oauth") :
+ libpq_gettext("could not find entry point for custom plugin");
return -1;
}
@@ -988,6 +1014,7 @@ extern int pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request);
static int
use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *request)
{
+ state->builtin = true;
return (pg_start_oauthbearer(conn, request) == 0) ? 1 : -1;
}
@@ -1041,10 +1068,7 @@ setup_token_request(PGconn *conn, fe_oauth_state *state)
poison_req_v2(&request, false);
}
if (res == 0)
- {
- state->builtin = true;
res = use_builtin_flow(conn, state, &request);
- }
if (res > 0)
{
diff --git a/src/test/modules/oauth_validator/meson.build b/src/test/modules/oauth_validator/meson.build
index 506a9894b8d..915603cbd29 100644
--- a/src/test/modules/oauth_validator/meson.build
+++ b/src/test/modules/oauth_validator/meson.build
@@ -50,6 +50,7 @@ test_install_libs += magic_validator
oauth_hook_client_sources = files(
'oauth_hook_client.c',
+ 'oauth_test_common.c',
)
if host_system == 'windows'
@@ -67,6 +68,19 @@ oauth_hook_client = executable('oauth_hook_client',
)
testprep_targets += oauth_hook_client
+oauth_flow = shared_module('oauth_flow',
+ files(
+ 'oauth_flow.c',
+ 'oauth_test_common.c',
+ ),
+ include_directories: [postgres_inc],
+ dependencies: [frontend_shlib_code, libpq],
+ kwargs: default_lib_args + {
+ 'install': false,
+ },
+)
+testprep_targets += oauth_flow
+
tests += {
'name': 'oauth_validator',
'sd': meson.current_source_dir(),
@@ -81,6 +95,7 @@ tests += {
'with_libcurl': oauth_flow_supported ? 'yes' : 'no',
'with_python': 'yes',
'cert_dir': meson.project_source_root() / 'src/test/ssl/ssl',
+ 'flow_module_path': oauth_flow.full_path(),
},
'deps': [oauth_hook_client],
},
diff --git a/src/test/modules/oauth_validator/Makefile b/src/test/modules/oauth_validator/Makefile
index 0b39a88fd9f..48b30aea24f 100644
--- a/src/test/modules/oauth_validator/Makefile
+++ b/src/test/modules/oauth_validator/Makefile
@@ -14,11 +14,13 @@ PGFILEDESC = "validator - test OAuth validator module"
PROGRAM = oauth_hook_client
PGAPPICON = win32
-OBJS = $(WIN32RES) oauth_hook_client.o
+OBJS = $(WIN32RES) oauth_hook_client.o oauth_test_common.o
PG_CPPFLAGS = -I$(libpq_srcdir)
PG_LIBS_INTERNAL += $(libpq_pgport)
+EXTRA_CLEAN = oauth_flow$(DLSUFFIX) oauth_flow.o
+
NO_INSTALLCHECK = 1
TAP_TESTS = 1
@@ -33,9 +35,15 @@ top_builddir = ../../../..
include $(top_builddir)/src/Makefile.global
include $(top_srcdir)/contrib/contrib-global.mk
+all: oauth_flow$(DLSUFFIX)
+
+oauth_flow$(DLSUFFIX): oauth_flow.o oauth_test_common.o
+ $(CC) $(CFLAGS) $^ $(LDFLAGS) $(libpq_pgport_shlib) $(LDFLAGS_SL) -shared -o $@
+
export PYTHON
export with_libcurl
export with_python
-export cert_dir=$(top_srcdir)/src/test/ssl/ssl
+export cert_dir := $(top_srcdir)/src/test/ssl/ssl
+export flow_module_path := $(abs_top_builddir)/$(subdir)/oauth_flow$(DLSUFFIX)
endif
diff --git a/src/test/modules/oauth_validator/oauth_test_common.h b/src/test/modules/oauth_validator/oauth_test_common.h
new file mode 100644
index 00000000000..33e72e30440
--- /dev/null
+++ b/src/test/modules/oauth_validator/oauth_test_common.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * oauth_test_common.h
+ * Shared functionality for oauth_hook_client and oauth_flow
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef OAUTH_TEST_COMMON_H
+#define OAUTH_TEST_COMMON_H
+
+/*
+ * Only public headers can be here, since oauth_flow.c is trying to test only
+ * the public API.
+ */
+#include "libpq-fe.h"
+
+extern int stress_async; /* for oauth_hook_client */
+
+extern char *oauth_test_parse_argv(int argc, char *argv[], int for_plugin);
+extern int oauth_test_authdata_hook(PGauthData type, PGconn *conn, void *data);
+extern int oauth_test_start_flow(PGconn *conn, PGoauthBearerRequestV2 *request);
+
+#endif /* OAUTH_TEST_COMMON_H */
diff --git a/src/test/modules/oauth_validator/oauth_flow.c b/src/test/modules/oauth_validator/oauth_flow.c
new file mode 100644
index 00000000000..9ef67f03eb3
--- /dev/null
+++ b/src/test/modules/oauth_validator/oauth_flow.c
@@ -0,0 +1,72 @@
+/*-------------------------------------------------------------------------
+ *
+ * oauth_flow.c
+ * Test plugin for clientside OAuth flows
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+/* Since we want to test the public API, only include public headers here. */
+#include "libpq-fe.h"
+#include "libpq-oauth.h"
+#include "oauth_test_common.h"
+
+static void
+load_test_flags(void)
+{
+ int argc;
+ char **argv;
+ char *env = getenv("OAUTH_TEST_FLAGS");
+ int flag_count;
+ int i;
+
+ if (!env || !env[0])
+ {
+ fprintf(stderr, "OAUTH_TEST_FLAGS must be set\n");
+ exit(1);
+ }
+
+ flag_count = 1;
+ for (char *c = env; *c; c++)
+ {
+ if (*c == '\x01')
+ flag_count++;
+ }
+
+ /* Slice OAUTH_TEST_FLAGS into a fake argv array. */
+ env = strdup(env);
+ argc = flag_count + 1;
+ argv = malloc(sizeof(*argv) * (argc + 1));
+
+ if (!env || !argv)
+ {
+ fprintf(stderr, "out of memory");
+ exit(1);
+ }
+
+ argv[0] = "[plugin test]";
+ for (i = 1; i < flag_count; i++)
+ {
+ argv[i] = env;
+
+ env = strchr(env, '\x01');
+ *env++ = '\0';
+ }
+ argv[flag_count] = env;
+ argv[argc] = NULL;
+
+ oauth_test_parse_argv(argc, argv, 1 /* plugin */ );
+}
+
+int
+pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request)
+{
+ load_test_flags();
+
+ return oauth_test_start_flow(conn, request);
+}
diff --git a/src/test/modules/oauth_validator/oauth_hook_client.c b/src/test/modules/oauth_validator/oauth_hook_client.c
index 4695d73e8f7..5f932acc571 100644
--- a/src/test/modules/oauth_validator/oauth_hook_client.c
+++ b/src/test/modules/oauth_validator/oauth_hook_client.c
@@ -18,143 +18,18 @@
#include <sys/socket.h>
-#include "getopt_long.h"
#include "libpq-fe.h"
-static int handle_auth_data(PGauthData type, PGconn *conn, void *data);
-static PostgresPollingStatusType async_cb(PGconn *conn,
- PGoauthBearerRequest *req,
- pgsocket *altsock);
-static PostgresPollingStatusType misbehave_cb(PGconn *conn,
- PGoauthBearerRequest *req,
- pgsocket *altsock);
-
-static void
-usage(char *argv[])
-{
- printf("usage: %s [flags] CONNINFO\n\n", argv[0]);
-
- printf("recognized flags:\n");
- printf(" -h, --help show this message\n");
- printf(" -v VERSION select the hook API version (default 2)\n");
- printf(" --expected-scope SCOPE fail if received scopes do not match SCOPE\n");
- printf(" --expected-uri URI fail if received configuration link does not match URI\n");
- printf(" --expected-issuer ISS fail if received issuer does not match ISS (v2 only)\n");
- printf(" --misbehave=MODE have the hook fail required postconditions\n"
- " (MODEs: no-hook, fail-async, no-token, no-socket)\n");
- printf(" --no-hook don't install OAuth hooks\n");
- printf(" --hang-forever don't ever return a token (combine with connect_timeout)\n");
- printf(" --token TOKEN use the provided TOKEN value\n");
- printf(" --error ERRMSG fail instead, with the given ERRMSG (v2 only)\n");
- printf(" --stress-async busy-loop on PQconnectPoll rather than polling\n");
-}
-
-/* --options */
-static bool no_hook = false;
-static bool hang_forever = false;
-static bool stress_async = false;
-static const char *expected_uri = NULL;
-static const char *expected_issuer = NULL;
-static const char *expected_scope = NULL;
-static const char *misbehave_mode = NULL;
-static char *token = NULL;
-static char *errmsg = NULL;
-static int hook_version = PQAUTHDATA_OAUTH_BEARER_TOKEN_V2;
+#include "oauth_test_common.h"
int
main(int argc, char *argv[])
{
- static const struct option long_options[] = {
- {"help", no_argument, NULL, 'h'},
-
- {"expected-scope", required_argument, NULL, 1000},
- {"expected-uri", required_argument, NULL, 1001},
- {"no-hook", no_argument, NULL, 1002},
- {"token", required_argument, NULL, 1003},
- {"hang-forever", no_argument, NULL, 1004},
- {"misbehave", required_argument, NULL, 1005},
- {"stress-async", no_argument, NULL, 1006},
- {"expected-issuer", required_argument, NULL, 1007},
- {"error", required_argument, NULL, 1008},
- {0}
- };
-
- const char *conninfo;
+ const char *conninfo = oauth_test_parse_argv(argc, argv, 0 /* hook */ );
PGconn *conn;
- int c;
-
- while ((c = getopt_long(argc, argv, "hv:", long_options, NULL)) != -1)
- {
- switch (c)
- {
- case 'h':
- usage(argv);
- return 0;
-
- case 'v':
- if (strcmp(optarg, "1") == 0)
- hook_version = PQAUTHDATA_OAUTH_BEARER_TOKEN;
- else if (strcmp(optarg, "2") == 0)
- hook_version = PQAUTHDATA_OAUTH_BEARER_TOKEN_V2;
- else
- {
- usage(argv);
- return 1;
- }
- break;
-
- case 1000: /* --expected-scope */
- expected_scope = optarg;
- break;
-
- case 1001: /* --expected-uri */
- expected_uri = optarg;
- break;
-
- case 1002: /* --no-hook */
- no_hook = true;
- break;
-
- case 1003: /* --token */
- token = optarg;
- break;
-
- case 1004: /* --hang-forever */
- hang_forever = true;
- break;
-
- case 1005: /* --misbehave */
- misbehave_mode = optarg;
- break;
-
- case 1006: /* --stress-async */
- stress_async = true;
- break;
-
- case 1007: /* --expected-issuer */
- expected_issuer = optarg;
- break;
-
- case 1008: /* --error */
- errmsg = optarg;
- break;
-
- default:
- usage(argv);
- return 1;
- }
- }
-
- if (argc != optind + 1)
- {
- usage(argv);
- return 1;
- }
-
- conninfo = argv[optind];
/* Set up our OAuth hooks. */
- PQsetAuthDataHook(handle_auth_data);
+ PQsetAuthDataHook(oauth_test_authdata_hook);
/* Connect. (All the actual work is in the hook.) */
if (stress_async)
@@ -192,190 +67,3 @@ main(int argc, char *argv[])
PQfinish(conn);
return 0;
}
-
-/*
- * PQauthDataHook implementation. Replaces the default client flow by handling
- * PQAUTHDATA_OAUTH_BEARER_TOKEN[_V2].
- */
-static int
-handle_auth_data(PGauthData type, PGconn *conn, void *data)
-{
- PGoauthBearerRequest *req;
- PGoauthBearerRequestV2 *req2 = NULL;
-
- Assert(hook_version == PQAUTHDATA_OAUTH_BEARER_TOKEN ||
- hook_version == PQAUTHDATA_OAUTH_BEARER_TOKEN_V2);
-
- if (no_hook || type != hook_version)
- return 0;
-
- req = data;
- if (type == PQAUTHDATA_OAUTH_BEARER_TOKEN_V2)
- req2 = data;
-
- if (hang_forever)
- {
- /* Start asynchronous processing. */
- req->async = async_cb;
- return 1;
- }
-
- if (misbehave_mode)
- {
- if (strcmp(misbehave_mode, "no-hook") != 0)
- req->async = misbehave_cb;
- return 1;
- }
-
- if (expected_uri)
- {
- if (!req->openid_configuration)
- {
- fprintf(stderr, "expected URI \"%s\", got NULL\n", expected_uri);
- return -1;
- }
-
- if (strcmp(expected_uri, req->openid_configuration) != 0)
- {
- fprintf(stderr, "expected URI \"%s\", got \"%s\"\n", expected_uri, req->openid_configuration);
- return -1;
- }
- }
-
- if (expected_scope)
- {
- if (!req->scope)
- {
- fprintf(stderr, "expected scope \"%s\", got NULL\n", expected_scope);
- return -1;
- }
-
- if (strcmp(expected_scope, req->scope) != 0)
- {
- fprintf(stderr, "expected scope \"%s\", got \"%s\"\n", expected_scope, req->scope);
- return -1;
- }
- }
-
- if (expected_issuer)
- {
- if (!req2)
- {
- fprintf(stderr, "--expected-issuer cannot be combined with -v1\n");
- return -1;
- }
-
- if (!req2->issuer)
- {
- fprintf(stderr, "expected issuer \"%s\", got NULL\n", expected_issuer);
- return -1;
- }
-
- if (strcmp(expected_issuer, req2->issuer) != 0)
- {
- fprintf(stderr, "expected issuer \"%s\", got \"%s\"\n", expected_issuer, req2->issuer);
- return -1;
- }
- }
-
- if (errmsg)
- {
- if (token)
- {
- fprintf(stderr, "--error cannot be combined with --token\n");
- return -1;
- }
- else if (!req2)
- {
- fprintf(stderr, "--error cannot be combined with -v1\n");
- return -1;
- }
-
- req2->error = errmsg;
- return -1;
- }
-
- req->token = token;
- return 1;
-}
-
-static PostgresPollingStatusType
-async_cb(PGconn *conn, PGoauthBearerRequest *req, pgsocket *altsock)
-{
- if (hang_forever)
- {
- /*
- * This code tests that nothing is interfering with libpq's handling
- * of connect_timeout.
- */
- static pgsocket sock = PGINVALID_SOCKET;
-
- if (sock == PGINVALID_SOCKET)
- {
- /* First call. Create an unbound socket to wait on. */
-#ifdef WIN32
- WSADATA wsaData;
- int err;
-
- err = WSAStartup(MAKEWORD(2, 2), &wsaData);
- if (err)
- {
- perror("WSAStartup failed");
- return PGRES_POLLING_FAILED;
- }
-#endif
- sock = socket(AF_INET, SOCK_DGRAM, 0);
- if (sock == PGINVALID_SOCKET)
- {
- perror("failed to create datagram socket");
- return PGRES_POLLING_FAILED;
- }
- }
-
- /* Make libpq wait on the (unreadable) socket. */
- *altsock = sock;
- return PGRES_POLLING_READING;
- }
-
- req->token = token;
- return PGRES_POLLING_OK;
-}
-
-static PostgresPollingStatusType
-misbehave_cb(PGconn *conn, PGoauthBearerRequest *req, pgsocket *altsock)
-{
- if (strcmp(misbehave_mode, "fail-async") == 0)
- {
- /* Just fail "normally". */
- if (errmsg)
- {
- PGoauthBearerRequestV2 *req2;
-
- if (hook_version == PQAUTHDATA_OAUTH_BEARER_TOKEN)
- {
- fprintf(stderr, "--error cannot be combined with -v1\n");
- exit(1);
- }
-
- req2 = (PGoauthBearerRequestV2 *) req;
- req2->error = errmsg;
- }
-
- return PGRES_POLLING_FAILED;
- }
- else if (strcmp(misbehave_mode, "no-token") == 0)
- {
- /* Callbacks must assign req->token before returning OK. */
- return PGRES_POLLING_OK;
- }
- else if (strcmp(misbehave_mode, "no-socket") == 0)
- {
- /* Callbacks must assign *altsock before asking for polling. */
- return PGRES_POLLING_READING;
- }
- else
- {
- fprintf(stderr, "unrecognized --misbehave mode: %s\n", misbehave_mode);
- exit(1);
- }
-}
diff --git a/src/test/modules/oauth_validator/oauth_test_common.c b/src/test/modules/oauth_validator/oauth_test_common.c
new file mode 100644
index 00000000000..cfcb4a354a8
--- /dev/null
+++ b/src/test/modules/oauth_validator/oauth_test_common.c
@@ -0,0 +1,373 @@
+/*-------------------------------------------------------------------------
+ *
+ * oauth_test_common.c
+ * Shared functionality for oauth_hook_client and oauth_flow
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/socket.h>
+
+#include "getopt_long.h"
+#include "libpq-fe.h"
+
+#include "oauth_test_common.h"
+
+static PostgresPollingStatusType async_cb(PGconn *conn,
+ PGoauthBearerRequest *req,
+ pgsocket *altsock);
+static PostgresPollingStatusType misbehave_cb(PGconn *conn,
+ PGoauthBearerRequest *req,
+ pgsocket *altsock);
+
+/* --options */
+static bool no_hook = false;
+static bool hang_forever = false;
+static const char *expected_uri = NULL;
+static const char *expected_issuer = NULL;
+static const char *expected_scope = NULL;
+static const char *misbehave_mode = NULL;
+static char *token = NULL;
+static char *errmsg = NULL;
+static int hook_version = PQAUTHDATA_OAUTH_BEARER_TOKEN_V2;
+
+/*
+ * XXX: stress_async is exported for the benefit of oauth_hook_client. Since
+ * we only use public headers (libpq-fe.h) for oauth_flow, it needs to be an int
+ * rather than a bool.
+ */
+int stress_async = false;
+
+static void
+usage(char *argv[])
+{
+ printf("usage: %s [flags] CONNINFO\n\n", argv[0]);
+
+ printf("recognized flags:\n");
+ printf(" -h, --help show this message\n");
+ printf(" -v VERSION select the hook API version (default 2)\n");
+ printf(" --expected-scope SCOPE fail if received scopes do not match SCOPE\n");
+ printf(" --expected-uri URI fail if received configuration link does not match URI\n");
+ printf(" --expected-issuer ISS fail if received issuer does not match ISS (v2 only)\n");
+ printf(" --misbehave=MODE have the hook fail required postconditions\n"
+ " (MODEs: no-hook, fail-async, no-token, no-socket)\n");
+ printf(" --no-hook don't install OAuth hooks\n");
+ printf(" --hang-forever don't ever return a token (combine with connect_timeout)\n");
+ printf(" --token TOKEN use the provided TOKEN value\n");
+ printf(" --error ERRMSG fail instead, with the given ERRMSG (v2 only)\n");
+ printf(" --stress-async busy-loop on PQconnectPoll rather than polling\n");
+}
+
+char *
+oauth_test_parse_argv(int argc, char *argv[], int for_plugin)
+{
+ static const struct option long_options[] = {
+ {"help", no_argument, NULL, 'h'},
+
+ {"expected-scope", required_argument, NULL, 1000},
+ {"expected-uri", required_argument, NULL, 1001},
+ {"no-hook", no_argument, NULL, 1002},
+ {"token", required_argument, NULL, 1003},
+ {"hang-forever", no_argument, NULL, 1004},
+ {"misbehave", required_argument, NULL, 1005},
+ {"stress-async", no_argument, NULL, 1006},
+ {"expected-issuer", required_argument, NULL, 1007},
+ {"error", required_argument, NULL, 1008},
+ {0}
+ };
+
+ int c;
+
+ if (for_plugin)
+ {
+ /* The "real" argv has already been parsed. Reset optind. */
+ optind = 1;
+ }
+
+ while ((c = getopt_long(argc, argv, "hv:", long_options, NULL)) != -1)
+ {
+ switch (c)
+ {
+ case 'h':
+ usage(argv);
+ exit(0);
+
+ case 'v':
+ if (strcmp(optarg, "1") == 0)
+ hook_version = PQAUTHDATA_OAUTH_BEARER_TOKEN;
+ else if (strcmp(optarg, "2") == 0)
+ hook_version = PQAUTHDATA_OAUTH_BEARER_TOKEN_V2;
+ else
+ {
+ usage(argv);
+ exit(1);
+ }
+ break;
+
+ case 1000: /* --expected-scope */
+ expected_scope = optarg;
+ break;
+
+ case 1001: /* --expected-uri */
+ expected_uri = optarg;
+ break;
+
+ case 1002: /* --no-hook */
+ no_hook = true;
+ break;
+
+ case 1003: /* --token */
+ token = optarg;
+ break;
+
+ case 1004: /* --hang-forever */
+ hang_forever = true;
+ break;
+
+ case 1005: /* --misbehave */
+ misbehave_mode = optarg;
+ break;
+
+ case 1006: /* --stress-async */
+ stress_async = true;
+ break;
+
+ case 1007: /* --expected-issuer */
+ expected_issuer = optarg;
+ break;
+
+ case 1008: /* --error */
+ errmsg = optarg;
+ break;
+
+ default:
+ usage(argv);
+ exit(1);
+ }
+ }
+
+ if (argc != (for_plugin ? optind : optind + 1))
+ {
+ usage(argv);
+ exit(1);
+ }
+
+ return argv[optind];
+}
+
+/*
+ * PQauthDataHook implementation. Replaces the default client flow by handling
+ * PQAUTHDATA_OAUTH_BEARER_TOKEN[_V2].
+ */
+int
+oauth_test_authdata_hook(PGauthData type, PGconn *conn, void *data)
+{
+ PGoauthBearerRequest *req;
+ PGoauthBearerRequestV2 *req2 = NULL;
+
+ Assert(hook_version == PQAUTHDATA_OAUTH_BEARER_TOKEN ||
+ hook_version == PQAUTHDATA_OAUTH_BEARER_TOKEN_V2);
+
+ if (no_hook || type != hook_version)
+ return 0;
+
+ req = data;
+ if (type == PQAUTHDATA_OAUTH_BEARER_TOKEN_V2)
+ req2 = data;
+
+ if (hang_forever)
+ {
+ /* Start asynchronous processing. */
+ req->async = async_cb;
+ return 1;
+ }
+
+ if (misbehave_mode)
+ {
+ if (strcmp(misbehave_mode, "no-hook") != 0)
+ req->async = misbehave_cb;
+ return 1;
+ }
+
+ if (expected_uri)
+ {
+ if (!req->openid_configuration)
+ {
+ fprintf(stderr, "expected URI \"%s\", got NULL\n", expected_uri);
+ return -1;
+ }
+
+ if (strcmp(expected_uri, req->openid_configuration) != 0)
+ {
+ fprintf(stderr, "expected URI \"%s\", got \"%s\"\n", expected_uri, req->openid_configuration);
+ return -1;
+ }
+ }
+
+ if (expected_scope)
+ {
+ if (!req->scope)
+ {
+ fprintf(stderr, "expected scope \"%s\", got NULL\n", expected_scope);
+ return -1;
+ }
+
+ if (strcmp(expected_scope, req->scope) != 0)
+ {
+ fprintf(stderr, "expected scope \"%s\", got \"%s\"\n", expected_scope, req->scope);
+ return -1;
+ }
+ }
+
+ if (expected_issuer)
+ {
+ if (!req2)
+ {
+ fprintf(stderr, "--expected-issuer cannot be combined with -v1\n");
+ return -1;
+ }
+
+ if (!req2->issuer)
+ {
+ fprintf(stderr, "expected issuer \"%s\", got NULL\n", expected_issuer);
+ return -1;
+ }
+
+ if (strcmp(expected_issuer, req2->issuer) != 0)
+ {
+ fprintf(stderr, "expected issuer \"%s\", got \"%s\"\n", expected_issuer, req2->issuer);
+ return -1;
+ }
+ }
+
+ if (errmsg)
+ {
+ if (token)
+ {
+ fprintf(stderr, "--error cannot be combined with --token\n");
+ return -1;
+ }
+ else if (!req2)
+ {
+ fprintf(stderr, "--error cannot be combined with -v1\n");
+ return -1;
+ }
+
+ req2->error = errmsg;
+ return -1;
+ }
+
+ req->token = token;
+ return 1;
+}
+
+/*
+ * Sets up a request for a plugin module (pg_start_oauthbearer()) rather than
+ * using the hook.
+ */
+int
+oauth_test_start_flow(PGconn *conn, PGoauthBearerRequestV2 *request)
+{
+ int ret;
+
+ /*
+ * We can still defer to the hook above to avoid copying code; we just
+ * have to translate the return value.
+ */
+ ret = oauth_test_authdata_hook(PQAUTHDATA_OAUTH_BEARER_TOKEN_V2, conn,
+ request);
+
+ if (ret == 0)
+ {
+ /* This is a bug in the test. */
+ fprintf(stderr, "plugin tests cannot make use of -v1 or --no-hook\n");
+ exit(1);
+ }
+
+ return (ret == 1) ? 0 : -1;
+}
+
+static PostgresPollingStatusType
+async_cb(PGconn *conn, PGoauthBearerRequest *req, pgsocket *altsock)
+{
+ if (hang_forever)
+ {
+ /*
+ * This code tests that nothing is interfering with libpq's handling
+ * of connect_timeout.
+ */
+ static pgsocket sock = PGINVALID_SOCKET;
+
+ if (sock == PGINVALID_SOCKET)
+ {
+ /* First call. Create an unbound socket to wait on. */
+#ifdef WIN32
+ WSADATA wsaData;
+ int err;
+
+ err = WSAStartup(MAKEWORD(2, 2), &wsaData);
+ if (err)
+ {
+ perror("WSAStartup failed");
+ return PGRES_POLLING_FAILED;
+ }
+#endif
+ sock = socket(AF_INET, SOCK_DGRAM, 0);
+ if (sock == PGINVALID_SOCKET)
+ {
+ perror("failed to create datagram socket");
+ return PGRES_POLLING_FAILED;
+ }
+ }
+
+ /* Make libpq wait on the (unreadable) socket. */
+ *altsock = sock;
+ return PGRES_POLLING_READING;
+ }
+
+ req->token = token;
+ return PGRES_POLLING_OK;
+}
+
+static PostgresPollingStatusType
+misbehave_cb(PGconn *conn, PGoauthBearerRequest *req, pgsocket *altsock)
+{
+ if (strcmp(misbehave_mode, "fail-async") == 0)
+ {
+ /* Just fail "normally". */
+ if (errmsg)
+ {
+ PGoauthBearerRequestV2 *req2;
+
+ if (hook_version == PQAUTHDATA_OAUTH_BEARER_TOKEN)
+ {
+ fprintf(stderr, "--error cannot be combined with -v1\n");
+ exit(1);
+ }
+
+ req2 = (PGoauthBearerRequestV2 *) req;
+ req2->error = errmsg;
+ }
+
+ return PGRES_POLLING_FAILED;
+ }
+ else if (strcmp(misbehave_mode, "no-token") == 0)
+ {
+ /* Callbacks must assign req->token before returning OK. */
+ return PGRES_POLLING_OK;
+ }
+ else if (strcmp(misbehave_mode, "no-socket") == 0)
+ {
+ /* Callbacks must assign *altsock before asking for polling. */
+ return PGRES_POLLING_READING;
+ }
+ else
+ {
+ fprintf(stderr, "unrecognized --misbehave mode: %s\n", misbehave_mode);
+ exit(1);
+ }
+}
diff --git a/src/test/modules/oauth_validator/t/002_client.pl b/src/test/modules/oauth_validator/t/002_client.pl
index dac684d7852..2f6f60331a1 100644
--- a/src/test/modules/oauth_validator/t/002_client.pl
+++ b/src/test/modules/oauth_validator/t/002_client.pl
@@ -1,6 +1,6 @@
#
# Exercises the API for custom OAuth client flows, using the oauth_hook_client
-# test driver.
+# test driver and the oauth_flow custom plugin.
#
# Copyright (c) 2021-2026, PostgreSQL Global Development Group
#
@@ -20,6 +20,10 @@ if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\boauth\b/)
'Potentially unsafe test oauth not enabled in PG_TEST_EXTRA';
}
+my $plugin_supported = (
+ check_pg_config("#define HAVE_SYS_EVENT_H 1")
+ or check_pg_config("#define HAVE_SYS_EPOLL_H 1"));
+
#
# Cluster Setup
#
@@ -72,6 +76,8 @@ sub test
$flags = $params{flags};
}
+ # First run the oauth_hook_client, which uses PQauthDataHook to insert a new
+ # OAuth flow.
my @cmd = ("oauth_hook_client", @{$flags}, $common_connstr);
note "running '" . join("' '", @cmd) . "'";
@@ -103,6 +109,37 @@ sub test
$node->log_check("$test_name: log matches",
$log_start, log_like => $params{log_like});
}
+
+ SKIP:
+ {
+ last SKIP if $params{hook_only};
+ skip "OAuth modules are not supported on this platform"
+ unless $plugin_supported;
+
+ # Run the same test with psql itself, loading the oauth_flow.so module.
+ local $ENV{PGOAUTHMODULE} = $ENV{flow_module_path};
+
+ # Flags are passed to the module via OAUTH_TEST_FLAGS, with 0x01 as a
+ # separator.
+ local $ENV{OAUTH_TEST_FLAGS} = join("\x01", @{$flags});
+
+ if ($params{expect_success})
+ {
+ $node->connect_ok(
+ $common_connstr,
+ "[plugin flow] $test_name",
+ expected_stderr => $params{expected_stderr},
+ log_like => $params{log_like});
+ }
+ else
+ {
+ $node->connect_fails(
+ $common_connstr,
+ "[plugin flow] $test_name",
+ expected_stderr => $params{expected_stderr},
+ log_like => $params{log_like});
+ }
+ }
}
test(
@@ -136,6 +173,7 @@ $common_connstr = "$base_connstr oauth_issuer=$issuer oauth_client_id=myID";
# Make sure the v1 hook continues to work.
test(
"v1 synchronous hook can provide a token",
+ hook_only => 1, # plugins don't support API v1
flags => [
"-v1",
"--token" => "my-token-v1",
@@ -150,6 +188,7 @@ if ($ENV{with_libcurl} ne 'yes')
# libpq should help users out if no OAuth support is built in.
test(
"fails without custom hook installed",
+ hook_only => 1, # plugins can't use --no-hook
flags => ["--no-hook"],
expected_stderr =>
qr/no OAuth flows are available \(try installing the libpq-oauth package\)/
@@ -206,6 +245,7 @@ foreach my $c (@cases)
test(
"hook misbehavior: $c->{'flag'} (v1)",
flags => [ '-v1', $c->{'flag'} ],
+ hook_only => 1, # plugins can't use -v1
expected_stderr => $c->{'expected_error'});
}
@@ -219,4 +259,19 @@ test(
expected_stderr =>
qr/user-defined OAuth flow failed: async error message/);
+SKIP:
+{
+ skip "OAuth modules are not supported on this platform"
+ unless $plugin_supported;
+
+ # Make sure a misaimed PGOAUTHMODULE gives the correct error message.
+ local $ENV{PGOAUTHMODULE} = $node->basedir . '/nonexistent.so';
+
+ $node->connect_fails(
+ $common_connstr,
+ "PGOAUTHMODULE error messages",
+ expected_stderr =>
+ qr/user-defined OAuth flow failed: plugin could not be loaded/);
+}
+
done_testing();
--
2.34.1
view thread (19+ messages) latest in thread
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected], [email protected], [email protected]
Subject: Re: [oauth] Stabilize the libpq-oauth ABI (and allow alternative implementations?)
In-Reply-To: <CAOYmi+=Pr7AAdkcKXyLw3ycxcrjGKsOV2CTYVV2PKYQw9ecG0Q@mail.gmail.com>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox