public inbox for [email protected]
help / color / mirror / Atom feedFrom: Zsolt Parragi <[email protected]>
To: PostgreSQL Hackers <[email protected]>
Subject: [oauth] Split and extend PGOAUTHDEBUG
Date: Wed, 18 Feb 2026 15:07:57 +0000
Message-ID: <CAN4CZFMmDZMH56O9vb_g7vHqAk8ryWFxBMV19C39PFghENg8kA@mail.gmail.com> (raw)
Hello!
I'm proposing 2 patches:
1 is the same patch I already sent as part of the PGOAUTHCAFILE
discussion[1], rebased on the current master: it splits
PGOAUTHDEBUG=UNSAFE into separate unsafe/safe settings which users can
toggle one by one.
2 is a new unsafe setting issuer-mismatch, which allows a connection
to continue if the client and server issuers don't match. While this
isn't useful for end users, it makes testing validators easier, as
validators authors should be able to verify that mismatched
configurations are rejected properly by the validator.
I based 2 on 1 because unconditionally adding this new unsafe option
would conflict with some tests. This way that test can use a limited
subset of PGOAUTHDEBUG and still work as intended.
Even in this form it is a best effort, as this is a debugging/testing flag:
a. If a custom client uses a custom PG_AUTHDATA_HOOK and provides a
custom token, libpq accepts any issues URL
b. If the issuer is a well known URI, used directly by libpq, it
accept the URL as is
c. if the url is not a well known URI, but doesn't match the server
URI - it doesn't work that nicely, it accepts the difference but
continues but retrieves the well known URI from the server, so ignores
the client setting
Technically this was already possible by a variation of (a) without
this patch, by implementing a custom client with a PG_AUTHDATA_HOOK,
providing a token from a different issuer to it, and lying about the
issuer to libpq (providing what the server expects). But that's not an
easy way to do it and requires all validators to implement custom
clients for testing.
Additionally this feature also could be useful for demoing that
validators are secure to users ("see, the validator rejects the
request even if we trick the client into continuing with
authentication")
[1] : https://www.postgresql.org/message-id/CAN4CZFNvZ9%2BpQ%3DOA4m%3DHcDgip84GHnekh4gUhYWfK3Q4%2BrBMxA%40...
Attachments:
[application/octet-stream] 0002-Add-new-PGOAUTHDEBUG-option-issuer-mismatch.patch (7.3K, 2-0002-Add-new-PGOAUTHDEBUG-option-issuer-mismatch.patch)
download | inline diff:
From 8f4331cfbb099e4b641a5b31f55a05be969915df Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <[email protected]>
Date: Wed, 18 Feb 2026 14:51:46 +0100
Subject: [PATCH 2/2] Add new PGOAUTHDEBUG option: issuer-mismatch
This new unsafe option allows to connection to proceed if the issuer
configured on the server and client mismatch, allowing to write
mismatched-issuer tests for validators.
Validators should test scenarios like this, as the wire allows this
situation, but previously libpq/psql prevented it, making writing tests
for this more difficult.
---
doc/src/sgml/libpq.sgml | 16 +++++++++++++++-
src/interfaces/libpq-oauth/oauth-curl.c | 13 +++++++++----
src/interfaces/libpq/fe-auth-oauth-debug.c | 7 +++++++
src/interfaces/libpq/fe-auth-oauth.c | 18 +++++++++++-------
src/interfaces/libpq/fe-auth-oauth.h | 1 +
.../modules/oauth_validator/t/001_server.pl | 15 +++++++++------
6 files changed, 52 insertions(+), 18 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 5d70cc2b261..6f29a4a7756 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -10559,6 +10559,20 @@ PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</linea
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>issuer-mismatch</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Tolerates a mismatch between the client's configured
+ <literal>oauth_issuer</literal> and the issuer found in the server's
+ discovery document. This disables the mix-up attack protection from
+ RFC 9207 and should only be used in development or testing environments
+ where the server's issuer identifier does not match the client
+ configuration.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>fast-retry</literal> (safe)</term>
<listitem>
@@ -10595,7 +10609,7 @@ PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</linea
<para>
Unsafe options (<literal>http</literal>, <literal>trace</literal>,
- <literal>custom-ca</literal>) require the <literal>UNSAFE:</literal> prefix.
+ <literal>custom-ca</literal>, <literal>issuer-mismatch</literal>) require the <literal>UNSAFE:</literal> prefix.
If unsafe options are specified without this prefix, a warning is printed
to standard error and that option is ignored. Other valid options in the
list continue to work. Safe options (<literal>fast-retry</literal>,
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index ac8b4631d53..d9512ef17dd 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -2223,10 +2223,15 @@ check_issuer(struct async_ctx *actx, PGconn *conn)
*/
if (strcmp(oauth_issuer_id, provider->issuer) != 0)
{
- actx_error(actx,
- "the issuer identifier (%s) does not match oauth_issuer (%s)",
- provider->issuer, oauth_issuer_id);
- return false;
+ if (!actx->debug_flags.issuer_mismatch)
+ {
+ actx_error(actx,
+ "the issuer identifier (%s) does not match oauth_issuer (%s)",
+ provider->issuer, oauth_issuer_id);
+ return false;
+ }
+
+ return true;
}
return true;
diff --git a/src/interfaces/libpq/fe-auth-oauth-debug.c b/src/interfaces/libpq/fe-auth-oauth-debug.c
index f65f069fed8..558b34da561 100644
--- a/src/interfaces/libpq/fe-auth-oauth-debug.c
+++ b/src/interfaces/libpq/fe-auth-oauth-debug.c
@@ -53,6 +53,12 @@ parse_debug_option(const char *option, oauth_debug_flags *flags, bool *is_unsafe
*is_unsafe = true;
return true;
}
+ else if (strcmp(option, "issuer-mismatch") == 0)
+ {
+ flags->issuer_mismatch = true;
+ *is_unsafe = true;
+ return true;
+ }
/* Safe options */
else if (strcmp(option, "fast-retry") == 0)
{
@@ -103,6 +109,7 @@ oauth_get_debug_flags(void)
flags.http = true;
flags.trace = true;
flags.custom_ca = true;
+ flags.issuer_mismatch = true;
flags.fast_retry = true;
flags.poll_counts = true;
flags.print_plugin_errors = true;
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index 5dff354c19b..b6d472a0330 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -606,13 +606,16 @@ handle_oauth_sasl_error(PGconn *conn, const char *msg, int msglen)
if (strcmp(conn->oauth_issuer_id, discovery_issuer) != 0)
{
- libpq_append_conn_error(conn,
- "server's discovery document at %s (issuer \"%s\") is incompatible with oauth_issuer (%s)",
- ctx.discovery_uri, discovery_issuer,
- conn->oauth_issuer_id);
+ if (!oauth_get_debug_flags().issuer_mismatch)
+ {
+ libpq_append_conn_error(conn,
+ "server's discovery document at %s (issuer \"%s\") is incompatible with oauth_issuer (%s)",
+ ctx.discovery_uri, discovery_issuer,
+ conn->oauth_issuer_id);
- free(discovery_issuer);
- goto cleanup;
+ free(discovery_issuer);
+ goto cleanup;
+ }
}
free(discovery_issuer);
@@ -625,7 +628,8 @@ handle_oauth_sasl_error(PGconn *conn, const char *msg, int msglen)
else
{
/* This must match the URI we'd previously determined. */
- if (strcmp(conn->oauth_discovery_uri, ctx.discovery_uri) != 0)
+ if (strcmp(conn->oauth_discovery_uri, ctx.discovery_uri) != 0
+ && !oauth_get_debug_flags().issuer_mismatch)
{
libpq_append_conn_error(conn,
"server's discovery document has moved to %s (previous location was %s)",
diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h
index 272638ea359..918681f16a5 100644
--- a/src/interfaces/libpq/fe-auth-oauth.h
+++ b/src/interfaces/libpq/fe-auth-oauth.h
@@ -52,6 +52,7 @@ typedef struct oauth_debug_flags
bool http; /* allow HTTP (unencrypted) connections */
bool trace; /* log HTTP traffic (exposes secrets) */
bool custom_ca; /* allow custom CA certificate file */
+ bool issuer_mismatch; /* tolerate issuer mismatch */
/* SAFE features - allowed without UNSAFE: prefix */
bool fast_retry; /* allow zero-second retry intervals */
diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl
index 6b649c0b06f..b587b51bb19 100644
--- a/src/test/modules/oauth_validator/t/001_server.pl
+++ b/src/test/modules/oauth_validator/t/001_server.pl
@@ -136,12 +136,15 @@ $node->connect_ok(
]);
# The issuer linked by the server must match the client's oauth_issuer setting.
-$node->connect_fails(
- "user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0636",
- "oauth_issuer must match discovery",
- expected_stderr =>
- qr@server's discovery document at \Q$issuer/.well-known/oauth-authorization-server/alternate\E \(issuer "\Q$issuer/alternate\E"\) is incompatible with oauth_issuer \(\Q$issuer\E\)@
-);
+{
+ local $ENV{PGOAUTHDEBUG} = "UNSAFE:http";
+ $node->connect_fails(
+ "user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0636",
+ "oauth_issuer must match discovery",
+ expected_stderr =>
+ qr@server's discovery document at \Q$issuer/.well-known/oauth-authorization-server/alternate\E \(issuer "\Q$issuer/alternate\E"\) is incompatible with oauth_issuer \(\Q$issuer\E\)@
+ );
+}
# Test require_auth settings against OAUTHBEARER.
my @cases = (
--
2.43.0
[application/octet-stream] 0001-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-options.patch (20.6K, 3-0001-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-options.patch)
download | inline diff:
From 9eec791d666bebf3735ccb286e6f044f391f85fd Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <[email protected]>
Date: Thu, 11 Dec 2025 23:56:08 +0000
Subject: [PATCH 1/2] Split PGOAUTHDEBUG=UNSAFE into multiple options
---
doc/src/sgml/libpq.sgml | 144 ++++++++++++++----
src/interfaces/libpq-oauth/Makefile | 12 +-
src/interfaces/libpq-oauth/meson.build | 6 +-
src/interfaces/libpq-oauth/oauth-curl.c | 18 +--
src/interfaces/libpq-oauth/oauth-utils.c | 11 --
src/interfaces/libpq-oauth/oauth-utils.h | 2 +-
src/interfaces/libpq-oauth/test-oauth-curl.c | 8 +-
src/interfaces/libpq/Makefile | 3 +-
src/interfaces/libpq/fe-auth-oauth-debug.c | 147 +++++++++++++++++++
src/interfaces/libpq/fe-auth-oauth.c | 16 +-
src/interfaces/libpq/fe-auth-oauth.h | 19 ++-
src/interfaces/libpq/meson.build | 1 +
12 files changed, 317 insertions(+), 70 deletions(-)
create mode 100644 src/interfaces/libpq/fe-auth-oauth-debug.c
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 21e1ba34a4e..5d70cc2b261 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -10508,41 +10508,123 @@ typedef struct PGoauthBearerRequest
<title>Debugging and Developer Settings</title>
<para>
- A "dangerous debugging mode" may be enabled by setting the environment
- variable <envar>PGOAUTHDEBUG=UNSAFE</envar>. This functionality is provided
- for ease of local development and testing only. It does several things that
- you will not want a production system to do:
+ Debug features may be enabled by setting the <envar>PGOAUTHDEBUG</envar>
+ environment variable. This functionality is provided for ease of local
+ development and testing. The variable accepts a comma-separated list of
+ debug options:
+
+ <programlisting>
+PGOAUTHDEBUG=option1,option2,... <lineannotation>for safe options only</lineannotation>
+PGOAUTHDEBUG=UNSAFE:option1,option2,... <lineannotation>when using unsafe options</lineannotation>
+PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</lineannotation>
+ </programlisting>
+ </para>
- <itemizedlist spacing="compact">
- <listitem>
- <para>
- permits the use of unencrypted HTTP during the OAuth provider exchange
- </para>
- </listitem>
- <listitem>
- <para>
- allows the system's trusted CA list to be completely replaced using the
- <envar>PGOAUTHCAFILE</envar> environment variable
- </para>
- </listitem>
- <listitem>
- <para>
- prints HTTP traffic (containing several critical secrets) to standard
- error during the OAuth flow
- </para>
- </listitem>
- <listitem>
- <para>
- permits the use of zero-second retry intervals, which can cause the
- client to busy-loop and pointlessly consume CPU
- </para>
- </listitem>
- </itemizedlist>
+ <para>
+ Available debug options:
+
+ <variablelist>
+ <varlistentry>
+ <term><literal>http</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Permits the use of unencrypted HTTP during the OAuth provider exchange.
+ This allows OAuth credentials to be transmitted over unencrypted
+ connections, which is extremely dangerous and should only be used for
+ local testing.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>trace</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Prints HTTP traffic to standard error during the OAuth flow. This output
+ contains critical secrets including bearer tokens, client secrets, access
+ tokens, and authorization codes. Never share this output with third
+ parties.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>custom-ca</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Allows the system's trusted CA list to be completely replaced using the
+ <envar>PGOAUTHCAFILE</envar> environment variable. This can facilitate
+ man-in-the-middle attacks when testing with self-signed certificates.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>fast-retry</literal> (safe)</term>
+ <listitem>
+ <para>
+ Permits the use of zero-second retry intervals instead of the normal
+ minimum of one second. This can speed up tests but may cause the client
+ to busy-loop and consume CPU unnecessarily.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>poll-counts</literal> (safe)</term>
+ <listitem>
+ <para>
+ Prints the total number of poll() calls to standard error when the
+ OAuth flow completes. This helps developers debug the async multiplexer
+ behavior.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>print-plugin-errors</literal> (safe)</term>
+ <listitem>
+ <para>
+ Prints plugin loading errors to standard error. This helps developers
+ and package maintainers debug issues when the OAuth plugin fails to load.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+
+ <para>
+ Unsafe options (<literal>http</literal>, <literal>trace</literal>,
+ <literal>custom-ca</literal>) require the <literal>UNSAFE:</literal> prefix.
+ If unsafe options are specified without this prefix, a warning is printed
+ to standard error and that option is ignored. Other valid options in the
+ list continue to work. Safe options (<literal>fast-retry</literal>,
+ <literal>poll-counts</literal>, <literal>print-plugin-errors</literal>) can
+ be used without the prefix.
+ </para>
+
+ <para>
+ Unrecognized option names will also trigger a warning and be ignored, while
+ valid options continue to work. This helps catch typos in the environment
+ variable configuration without breaking the debugging of valid options.
</para>
+
+ <para>
+ Examples:
+ <programlisting>
+PGOAUTHDEBUG=fast-retry,poll-counts <lineannotation>safe options only</lineannotation>
+PGOAUTHDEBUG=UNSAFE:http,trace <lineannotation>enable HTTP and traffic logging</lineannotation>
+PGOAUTHDEBUG=UNSAFE:http,custom-ca,poll-counts <lineannotation>mix of unsafe and safe</lineannotation>
+PGOAUTHDEBUG=UNSAFE <lineannotation>legacy; enables all options</lineannotation>
+ </programlisting>
+ </para>
+
<warning>
<para>
- Do not share the output of the OAuth flow traffic with third parties. It
- contains secrets that can be used to attack your clients and servers.
+ Never use unsafe debug options in production environments. The
+ <literal>trace</literal> option in particular exposes secrets that can be
+ used to attack your clients and servers. Do not share the output with third
+ parties.
</para>
</warning>
</sect2>
diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile
index a5f2d65fcad..60ce0ad8c58 100644
--- a/src/interfaces/libpq-oauth/Makefile
+++ b/src/interfaces/libpq-oauth/Makefile
@@ -30,15 +30,25 @@ override CFLAGS += $(PTHREAD_CFLAGS)
OBJS = \
$(WIN32RES)
-OBJS_STATIC = oauth-curl.o
+OBJS_STATIC = \
+ oauth-curl.o \
+ fe-auth-oauth-debug.o
# The shared library needs additional glue symbols.
OBJS_SHLIB = \
oauth-curl_shlib.o \
oauth-utils.o \
+ fe-auth-oauth-debug_shlib.o
oauth-utils.o: override CPPFLAGS += -DUSE_DYNAMIC_OAUTH
oauth-curl_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH
+fe-auth-oauth-debug_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH
+
+fe-auth-oauth-debug.o: $(libpq_srcdir)/fe-auth-oauth-debug.c
+ $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
+
+fe-auth-oauth-debug_shlib.o: $(libpq_srcdir)/fe-auth-oauth-debug.c
+ $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@
# Add shlib-/stlib-specific objects.
$(shlib): override OBJS += $(OBJS_SHLIB)
diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build
index d8a0c04095a..86a10ccca27 100644
--- a/src/interfaces/libpq-oauth/meson.build
+++ b/src/interfaces/libpq-oauth/meson.build
@@ -6,6 +6,7 @@ endif
libpq_oauth_sources = files(
'oauth-curl.c',
+ '../libpq/fe-auth-oauth-debug.c',
)
# The shared library needs additional glue symbols.
@@ -50,7 +51,10 @@ libpq_oauth_so = shared_module(libpq_oauth_name,
libpq_oauth_test_deps = []
-oauth_test_sources = files('test-oauth-curl.c') + libpq_oauth_so_sources
+oauth_test_sources = files(
+ 'test-oauth-curl.c',
+ '../libpq/fe-auth-oauth-debug.c',
+) + libpq_oauth_so_sources
if host_system == 'windows'
oauth_test_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 691e7ec1d9f..ac8b4631d53 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -278,7 +278,7 @@ struct async_ctx
int running; /* is asynchronous work in progress? */
bool user_prompted; /* have we already sent the authz prompt? */
bool used_basic_auth; /* did we send a client secret? */
- bool debugging; /* can we give unsafe developer assistance? */
+ oauth_debug_flags debug_flags; /* can we give developer assistance */
int dbg_num_calls; /* (debug mode) how many times were we called? */
};
@@ -985,7 +985,7 @@ parse_interval(struct async_ctx *actx, const char *interval_str)
parsed = ceil(parsed);
if (parsed < 1)
- return actx->debugging ? 0 : 1;
+ return actx->debug_flags.fast_retry ? 0 : 1;
else if (parsed >= INT_MAX)
return INT_MAX;
@@ -1759,7 +1759,7 @@ setup_curl_handles(struct async_ctx *actx)
*/
CHECK_SETOPT(actx, CURLOPT_NOSIGNAL, 1L, return false);
- if (actx->debugging)
+ if (actx->debug_flags.trace)
{
/*
* Set a callback for retrieving error information from libcurl, the
@@ -1791,7 +1791,7 @@ setup_curl_handles(struct async_ctx *actx)
const long unsafe = CURLPROTO_HTTPS | CURLPROTO_HTTP;
#endif
- if (actx->debugging)
+ if (actx->debug_flags.http)
protos = unsafe;
CHECK_SETOPT(actx, popt, protos, return false);
@@ -1805,7 +1805,7 @@ setup_curl_handles(struct async_ctx *actx)
* the flow to work at all, so any changes to the roots are likely to be
* done system-wide.
*/
- if (actx->debugging)
+ if (actx->debug_flags.custom_ca)
{
const char *env;
@@ -2271,7 +2271,7 @@ check_for_device_flow(struct async_ctx *actx)
* decent time to bail out if we're not using HTTPS for the endpoints
* we'll use for the flow.
*/
- if (!actx->debugging)
+ if (!actx->debug_flags.http)
{
if (pg_strncasecmp(provider->device_authorization_endpoint,
HTTPS_SCHEME, strlen(HTTPS_SCHEME)) != 0)
@@ -2793,8 +2793,8 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
actx->mux = PGINVALID_SOCKET;
actx->timerfd = -1;
- /* Should we enable unsafe features? */
- actx->debugging = oauth_unsafe_debugging_enabled();
+ /* Parse debug flags from environment */
+ actx->debug_flags = oauth_get_debug_flags();
state->async_ctx = actx;
@@ -3074,7 +3074,7 @@ pg_fe_run_oauth_flow(PGconn *conn)
actx = state->async_ctx;
Assert(actx || result == PGRES_POLLING_FAILED);
- if (actx && actx->debugging)
+ if (actx && actx->debug_flags.poll_counts)
{
actx->dbg_num_calls++;
if (result == PGRES_POLLING_OK || result == PGRES_POLLING_FAILED)
diff --git a/src/interfaces/libpq-oauth/oauth-utils.c b/src/interfaces/libpq-oauth/oauth-utils.c
index 4ebe7d0948c..fab8990b746 100644
--- a/src/interfaces/libpq-oauth/oauth-utils.c
+++ b/src/interfaces/libpq-oauth/oauth-utils.c
@@ -142,17 +142,6 @@ libpq_gettext(const char *msgid)
#endif /* ENABLE_NLS */
-/*
- * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment.
- */
-bool
-oauth_unsafe_debugging_enabled(void)
-{
- const char *env = getenv("PGOAUTHDEBUG");
-
- return (env && strcmp(env, "UNSAFE") == 0);
-}
-
/*
* Duplicate SOCK_ERRNO* definitions from libpq-int.h, for use by
* pq_block/reset_sigpipe().
diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h
index 9f4d5b692d2..4d986fcb358 100644
--- a/src/interfaces/libpq-oauth/oauth-utils.h
+++ b/src/interfaces/libpq-oauth/oauth-utils.h
@@ -76,7 +76,7 @@ typedef enum
} 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 oauth_debug_flags oauth_get_debug_flags(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-oauth/test-oauth-curl.c b/src/interfaces/libpq-oauth/test-oauth-curl.c
index 4328a332738..d9971797b5c 100644
--- a/src/interfaces/libpq-oauth/test-oauth-curl.c
+++ b/src/interfaces/libpq-oauth/test-oauth-curl.c
@@ -89,7 +89,13 @@ init_test_actx(void)
actx->mux = PGINVALID_SOCKET;
actx->timerfd = -1;
- actx->debugging = true;
+ actx->debug_flags.http = true;
+ actx->debug_flags.trace = true;
+ actx->debug_flags.custom_ca = true;
+ actx->debug_flags.issuer_mismatch = true;
+ actx->debug_flags.fast_retry = true;
+ actx->debug_flags.poll_counts = true;
+ actx->debug_flags.print_plugin_errors = true;
initPQExpBuffer(&actx->errbuf);
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index bf4baa92917..1165859859c 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -44,7 +44,8 @@ OBJS = \
legacy-pqsignal.o \
libpq-events.o \
pqexpbuffer.o \
- fe-auth.o
+ fe-auth.o \
+ fe-auth-oauth-debug.o
# File shared across all SSL implementations supported.
ifneq ($(with_ssl),no)
diff --git a/src/interfaces/libpq/fe-auth-oauth-debug.c b/src/interfaces/libpq/fe-auth-oauth-debug.c
new file mode 100644
index 00000000000..f65f069fed8
--- /dev/null
+++ b/src/interfaces/libpq/fe-auth-oauth-debug.c
@@ -0,0 +1,147 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-auth-oauth-debug.c
+ * Parsing logic for PGOAUTHDEBUG environment variable
+ *
+ * This file contains pure string parsing logic with no dependencies on
+ * libpq or libpq-oauth implementation details. It's compiled into both
+ * libraries to avoid code duplication.
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ * src/interfaces/libpq/fe-auth-oauth-debug.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fe-auth-oauth.h"
+
+/*
+ * Parse a single debug option from PGOAUTHDEBUG.
+ * Returns true if the option is recognized, false otherwise.
+ * Sets *is_unsafe to indicate if this option requires the UNSAFE: prefix.
+ */
+static bool
+parse_debug_option(const char *option, oauth_debug_flags *flags, bool *is_unsafe)
+{
+ *is_unsafe = false;
+
+ /* Unsafe options */
+ if (strcmp(option, "http") == 0)
+ {
+ flags->http = true;
+ *is_unsafe = true;
+ return true;
+ }
+ else if (strcmp(option, "trace") == 0)
+ {
+ flags->trace = true;
+ *is_unsafe = true;
+ return true;
+ }
+ else if (strcmp(option, "custom-ca") == 0)
+ {
+ flags->custom_ca = true;
+ *is_unsafe = true;
+ return true;
+ }
+ /* Safe options */
+ else if (strcmp(option, "fast-retry") == 0)
+ {
+ flags->fast_retry = true;
+ return true;
+ }
+ else if (strcmp(option, "poll-counts") == 0)
+ {
+ flags->poll_counts = true;
+ return true;
+ }
+ else if (strcmp(option, "print-plugin-errors") == 0)
+ {
+ flags->print_plugin_errors = true;
+ return true;
+ }
+
+ return false;
+}
+
+/*
+ * Parses the PGOAUTHDEBUG environment variable and returns debug flags.
+ *
+ * Supported formats:
+ * PGOAUTHDEBUG=UNSAFE - legacy format, enables all features
+ * PGOAUTHDEBUG=option1,option2 - enable safe features only
+ * PGOAUTHDEBUG=UNSAFE:opt1,opt2 - enable unsafe and/or safe features
+ *
+ * Prints a warning and skips the invalid option if:
+ * - An unrecognized option is specified
+ * - An unsafe option is specified without the UNSAFE: prefix
+ */
+oauth_debug_flags
+oauth_get_debug_flags(void)
+{
+ oauth_debug_flags flags = {0};
+ const char *env = getenv("PGOAUTHDEBUG");
+ char *options_str;
+ char *option;
+ char *saveptr = NULL;
+ bool unsafe_prefix = false;
+
+ if (!env || env[0] == '\0')
+ return flags;
+
+ if (strcmp(env, "UNSAFE") == 0)
+ {
+ flags.http = true;
+ flags.trace = true;
+ flags.custom_ca = true;
+ flags.fast_retry = true;
+ flags.poll_counts = true;
+ flags.print_plugin_errors = true;
+ return flags;
+ }
+
+ if (strncmp(env, "UNSAFE:", 7) == 0)
+ {
+ unsafe_prefix = true;
+ env += 7;
+ }
+
+ options_str = strdup(env);
+ if (!options_str)
+ return flags;
+
+ option = strtok_r(options_str, ",", &saveptr);
+ while (option != NULL)
+ {
+ bool is_unsafe;
+
+ if (!parse_debug_option(option, &flags, &is_unsafe))
+ {
+ fprintf(stderr,
+ "WARNING: PGOAUTHDEBUG: unrecognized debug option \"%s\" (ignored)\n",
+ option);
+ }
+ else if (is_unsafe && !unsafe_prefix)
+ {
+ fprintf(stderr,
+ "WARNING: PGOAUTHDEBUG: unsafe option \"%s\" requires UNSAFE: prefix (ignored)\n"
+ "Use: PGOAUTHDEBUG=UNSAFE:%s\n",
+ option, option);
+ }
+
+ option = strtok_r(NULL, ",", &saveptr);
+ }
+
+ free(options_str);
+
+ return flags;
+}
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index 67879d64b39..5dff354c19b 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -383,7 +383,7 @@ issuer_from_well_known_uri(PGconn *conn, const char *wkuri)
authority_start = wkuri + strlen(HTTPS_SCHEME);
if (!authority_start
- && oauth_unsafe_debugging_enabled()
+ && oauth_get_debug_flags().http
&& pg_strncasecmp(wkuri, HTTP_SCHEME, strlen(HTTP_SCHEME)) == 0)
{
/* Allow http:// for testing only. */
@@ -877,7 +877,7 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state)
*
* Note that POSIX dlerror() isn't guaranteed to be threadsafe.
*/
- if (oauth_unsafe_debugging_enabled())
+ if (oauth_get_debug_flags().print_plugin_errors)
fprintf(stderr, "failed dlopen for libpq-oauth: %s\n", dlerror());
return false;
@@ -891,7 +891,7 @@ 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.
*/
- if (oauth_unsafe_debugging_enabled())
+ if (oauth_get_debug_flags().print_plugin_errors)
fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
dlclose(state->builtin_flow);
@@ -1392,13 +1392,3 @@ pqClearOAuthToken(PGconn *conn)
conn->oauth_token = NULL;
}
-/*
- * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment.
- */
-bool
-oauth_unsafe_debugging_enabled(void)
-{
- const char *env = getenv("PGOAUTHDEBUG");
-
- return (env && strcmp(env, "UNSAFE") == 0);
-}
diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h
index 5c8a24b76fa..272638ea359 100644
--- a/src/interfaces/libpq/fe-auth-oauth.h
+++ b/src/interfaces/libpq/fe-auth-oauth.h
@@ -42,8 +42,25 @@ typedef struct
void *builtin_flow;
} fe_oauth_state;
+/*
+ * Debug flags for PGOAUTHDEBUG environment variable.
+ * Each flag controls a specific debug feature.
+ */
+typedef struct oauth_debug_flags
+{
+ /* UNSAFE features - require UNSAFE: prefix */
+ bool http; /* allow HTTP (unencrypted) connections */
+ bool trace; /* log HTTP traffic (exposes secrets) */
+ bool custom_ca; /* allow custom CA certificate file */
+
+ /* SAFE features - allowed without UNSAFE: prefix */
+ bool fast_retry; /* allow zero-second retry intervals */
+ bool poll_counts; /* print poll() statistics */
+ bool print_plugin_errors; /* print plugin loading errors */
+} oauth_debug_flags;
+
extern void pqClearOAuthToken(PGconn *conn);
-extern bool oauth_unsafe_debugging_enabled(void);
+extern oauth_debug_flags oauth_get_debug_flags(void);
extern bool use_builtin_flow(PGconn *conn, fe_oauth_state *state);
/* Mechanisms in fe-auth-oauth.c */
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index c5ecd9c3a87..7f2999aebb3 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -2,6 +2,7 @@
libpq_sources = files(
'fe-auth-oauth.c',
+ 'fe-auth-oauth-debug.c',
'fe-auth-scram.c',
'fe-auth.c',
'fe-cancel.c',
--
2.43.0
view thread (13+ 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]
Subject: Re: [oauth] Split and extend PGOAUTHDEBUG
In-Reply-To: <CAN4CZFMmDZMH56O9vb_g7vHqAk8ryWFxBMV19C39PFghENg8kA@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