From 933f6432f8743287977e9da99670c582b91275ae Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <zsolt.parragi@cancellar.hu>
Date: Wed, 18 Feb 2026 14:51:46 +0100
Subject: [PATCH v2 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                        | 18 ++++++++++++++++--
 src/interfaces/libpq/fe-auth-oauth.h           |  1 +
 src/interfaces/libpq-oauth/oauth-curl.c        | 13 +++++++++----
 src/interfaces/libpq-oauth/test-oauth-curl.c   |  1 +
 src/interfaces/libpq/fe-auth-oauth-debug.c     |  7 +++++++
 src/interfaces/libpq/fe-auth-oauth.c           | 18 +++++++++++-------
 .../modules/oauth_validator/t/001_server.pl    | 15 +++++++++------
 7 files changed, 54 insertions(+), 19 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 2e5fb9011e9..172f8138546 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -10683,6 +10683,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>
@@ -10718,8 +10732,8 @@ PGOAUTHDEBUG=UNSAFE    <lineannotation>legacy format; enables all options</linea
    </para>
 
    <para>
-    Unsafe options (<literal>http</literal>, <literal>trace</literal>)
-    require the <literal>UNSAFE:</literal> prefix.
+    Unsafe options (<literal>http</literal>, <literal>trace</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/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h
index fde5c30c013..acc678f91cf 100644
--- a/src/interfaces/libpq/fe-auth-oauth.h
+++ b/src/interfaces/libpq/fe-auth-oauth.h
@@ -47,6 +47,7 @@ typedef struct oauth_debug_flags
 	/* UNSAFE features - require UNSAFE: prefix */
 	bool		http;			/* allow HTTP (unencrypted) connections */
 	bool		trace;			/* log HTTP traffic (exposes secrets) */
+	bool		issuer_mismatch;	/* tolerate issuer mismatch */
 
 	/* SAFE features - allowed without UNSAFE: prefix */
 	bool		fast_retry;		/* allow zero-second retry intervals */
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 564d76cf063..5c18702da62 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -2249,10 +2249,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-oauth/test-oauth-curl.c b/src/interfaces/libpq-oauth/test-oauth-curl.c
index 06815be9a0a..776eaaafc8d 100644
--- a/src/interfaces/libpq-oauth/test-oauth-curl.c
+++ b/src/interfaces/libpq-oauth/test-oauth-curl.c
@@ -91,6 +91,7 @@ init_test_actx(void)
 	actx->timerfd = -1;
 	actx->debug_flags.http = true;
 	actx->debug_flags.trace = 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;
diff --git a/src/interfaces/libpq/fe-auth-oauth-debug.c b/src/interfaces/libpq/fe-auth-oauth-debug.c
index f9a1b1f195f..309286e253f 100644
--- a/src/interfaces/libpq/fe-auth-oauth-debug.c
+++ b/src/interfaces/libpq/fe-auth-oauth-debug.c
@@ -47,6 +47,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)
 	{
@@ -96,6 +102,7 @@ oauth_get_debug_flags(void)
 	{
 		flags.http = true;
 		flags.trace = 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 4bfe31b03cb..41340530f3e 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/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl
index 9e4dba8c924..abe47154529 100644
--- a/src/test/modules/oauth_validator/t/001_server.pl
+++ b/src/test/modules/oauth_validator/t/001_server.pl
@@ -190,12 +190,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.34.1

