From 8f4331cfbb099e4b641a5b31f55a05be969915df 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 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

