Hello!
On Thu, 2026-03-19 at 20:15 +0000, Zsolt Parragi wrote:
> Thanks, v5 looks good!
> 
> (One documentation comment I missed previously: the oauth_ca_file
> connection parameter should also be documented, but that's just the
> same documentation repeated at one more place)

Good point! attached with the new doc and updated reviewers list (sorry
Jacob I forgot you the first time)

Regards!

-- 
Jonathan Gonzalez V. 
EDB: https://www.enterprisedb.com
From 32f3f1163a061c3a512e05dfe55e7c643e73fcf8 Mon Sep 17 00:00:00 2001
From: "Jonathan Gonzalez V." <[email protected]>
Date: Wed, 29 Oct 2025 16:54:42 +0100
Subject: [PATCH v6 1/1] libpq-oauth: allow changing the CA when not in debug
 mode

Allowing to set a CA enables users environment like companies with
internal CA or developers working on their own local system while
using a self-signed CA and don't need to see all the debug messages
while testing inside an internal environment.

Reviewed-by: Jacob Champion <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Signed-off-by: Jonathan Gonzalez V. <[email protected]>
---
 doc/src/sgml/libpq.sgml                       | 33 ++++++++++++---
 src/interfaces/libpq-oauth/oauth-curl.c       | 27 +++++++------
 src/interfaces/libpq/fe-connect.c             |  5 +++
 src/interfaces/libpq/libpq-int.h              |  1 +
 .../modules/oauth_validator/t/001_server.pl   | 40 ++++++++++++++++++-
 .../modules/oauth_validator/t/OAuth/Server.pm |  2 +-
 6 files changed, 86 insertions(+), 22 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 6db823808fc..cb836abc978 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2585,6 +2585,16 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-oauth-ca-file" xreflabel="oauth_ca_file">
+      <term><literal>oauth_ca_file</literal></term>
+      <listitem>
+       <para>
+        Allows to specify the path to a CA file that will be used by the client
+        to verify the certificate from the OAuth server side.
+       </para>
+      </listitem>
+     </varlistentry>
+
     </variablelist>
    </para>
   </sect2>
@@ -10620,12 +10630,6 @@ typedef struct
        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
@@ -10647,6 +10651,23 @@ typedef struct
     </para>
    </warning>
   </sect2>
+  <sect2 id="libpq-oauth-environment">
+   <title>Environment variables</title>
+   <para>
+    The behavior of the OAuth calls may be affected by the following variables:
+    <variablelist>
+     <varlistentry>
+      <term><envar>PGOAUTHCAFILE</envar></term>
+      <listitem>
+       <para>
+        Allows to specify the path to a CA file that will be used by the client
+        to verify the certificate from the OAuth server side.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+  </sect2>
  </sect1>
 
 
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 052ecd32da2..561875d6db1 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -17,6 +17,7 @@
 
 #include <curl/curl.h>
 #include <math.h>
+#include <string.h>
 #include <unistd.h>
 
 #if defined(HAVE_SYS_EPOLL_H)
@@ -216,6 +217,7 @@ 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) */
+	char	   *ca_file;		/* oauth_ca_file */
 
 	/* options cached from the PGoauthBearerRequest (we don't own these) */
 	const char *discovery_uri;
@@ -336,6 +338,7 @@ free_async_ctx(struct async_ctx *actx)
 
 	free(actx->client_id);
 	free(actx->client_secret);
+	free(actx->ca_file);
 
 	free(actx);
 }
@@ -1834,20 +1837,12 @@ setup_curl_handles(struct async_ctx *actx)
 	}
 
 	/*
-	 * If we're in debug mode, allow the developer to change the trusted CA
-	 * list. For now, this is not something we expose outside of the UNSAFE
-	 * mode, because it's not clear that it's useful in production: both libpq
-	 * and the user's browser must trust the same authorization servers for
-	 * the flow to work at all, so any changes to the roots are likely to be
-	 * done system-wide.
+	 * Allow to set the CA even if we're not in debug mode, this would make it
+	 * easy to work on environments where the CA could be internal and
+	 * available on every system, like big companies with airgap systems.
 	 */
-	if (actx->debugging)
-	{
-		const char *env;
-
-		if ((env = getenv("PGOAUTHCAFILE")) != NULL)
-			CHECK_SETOPT(actx, CURLOPT_CAINFO, env, return false);
-	}
+	if (actx->ca_file != NULL)
+		CHECK_SETOPT(actx, CURLOPT_CAINFO, actx->ca_file, return false);
 
 	/*
 	 * Suppress the Accept header to make our request as minimal as possible.
@@ -3125,6 +3120,12 @@ pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request)
 			if (!actx->client_secret)
 				goto oom;
 		}
+		else if (strcmp(opt->keyword, "oauth_ca_file") == 0)
+		{
+			actx->ca_file = strdup(opt->val);
+			if (!actx->ca_file)
+				goto oom;
+		}
 	}
 
 	PQconninfoFree(conninfo);
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index db9b4c8edbf..4f3af722881 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -413,6 +413,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"OAuth-Scope", "", 15,
 	offsetof(struct pg_conn, oauth_scope)},
 
+	{"oauth_ca_file", "PGOAUTHCAFILE", NULL, NULL,
+	 "OAuth-CA-File", "", 64,
+	 offsetof(struct pg_conn, oauth_ca_file)},
+
 	{"sslkeylogfile", NULL, NULL, NULL,
 		"SSL-Key-Log-File", "D", 64,
 	offsetof(struct pg_conn, sslkeylogfile)},
@@ -5158,6 +5162,7 @@ freePGconn(PGconn *conn)
 	free(conn->oauth_discovery_uri);
 	free(conn->oauth_client_id);
 	free(conn->oauth_client_secret);
+	free(conn->oauth_ca_file);
 	free(conn->oauth_scope);
 	/* Note that conn->Pfdebug is not ours to close or free */
 	free(conn->events);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index bd7eb59f5f8..1f1fb89e02f 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -444,6 +444,7 @@ struct pg_conn
 	char	   *oauth_client_secret;	/* client secret */
 	char	   *oauth_scope;	/* access token scope */
 	char	   *oauth_token;	/* access token */
+	char       *oauth_ca_file;	/* CA file path  */
 	bool		oauth_want_retry;	/* should we retry on failure? */
 
 	/* Optional file to write trace info to */
diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl
index cdad2ae8011..b66d99dd4bb 100644
--- a/src/test/modules/oauth_validator/t/001_server.pl
+++ b/src/test/modules/oauth_validator/t/001_server.pl
@@ -137,10 +137,46 @@ $node->connect_fails(
 	expected_stderr =>
 	  qr/failed to fetch OpenID discovery document:.*peer certificate/i);
 
-# Now we can use our alternative CA.
-$ENV{PGOAUTHCAFILE} = "$ENV{cert_dir}/root+server_ca.crt";
+# Make sure that PGOAUTHDEBUG is not required to specify the certificate
+delete $ENV{PGOAUTHDEBUG};
 
+# The alternative CA path to use during the tests
+my $alternative_ca = "$ENV{cert_dir}/root+server_ca.crt";
+
+# Make sure we can use oauth_ca_file option to specify the alternative CA path
 my $user = "test";
+$node->connect_ok(
+	"user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635 oauth_ca_file=$alternative_ca",
+	"connect as test",
+	expected_stderr =>
+	  qr@Visit https://example\.com/ and enter the code: postgresuser@,
+	log_like => [
+		qr/oauth_validator: token="9243959234", role="$user"/,
+		qr/oauth_validator: issuer="\Q$issuer\E", scope="openid postgres"/,
+		qr/connection authenticated: identity="test" method=oauth/,
+		qr/connection authorized/,
+	]);
+
+# Make sure that we can use the environment variable without the PGOAUTHDEBUG
+# and use it for the rest of the tests
+$ENV{PGOAUTHCAFILE} = $alternative_ca;
+
+$node->connect_ok(
+	"user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
+	"connect as test",
+	expected_stderr =>
+	  qr@Visit https://example\.com/ and enter the code: postgresuser@,
+	log_like => [
+		qr/oauth_validator: token="9243959234", role="$user"/,
+		qr/oauth_validator: issuer="\Q$issuer\E", scope="openid postgres"/,
+		qr/connection authenticated: identity="test" method=oauth/,
+		qr/connection authorized/,
+	]);
+
+# Enable PGOAUTHDEBUG=UNSAFE to have the proper count later with the `[libpq] total number of polls` messages
+$ENV{PGOAUTHDEBUG} = "UNSAFE";
+
+$user = "test";
 $node->connect_ok(
 	"user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
 	"connect as test",
diff --git a/src/test/modules/oauth_validator/t/OAuth/Server.pm b/src/test/modules/oauth_validator/t/OAuth/Server.pm
index d923d4c5eb2..62a29c283df 100644
--- a/src/test/modules/oauth_validator/t/OAuth/Server.pm
+++ b/src/test/modules/oauth_validator/t/OAuth/Server.pm
@@ -28,7 +28,7 @@ daemon implemented in t/oauth_server.py. (Python has a fairly usable HTTP server
 in its standard library, so the implementation was ported from Perl.)
 
 This authorization server serves HTTPS on 127.0.0.1 (IPv4 only). libpq will need
-to set PGOAUTHDEBUG=UNSAFE and PGOAUTHCAFILE with the right CA.
+to set PGOAUTHCAFILE with the right CA.
 
 =cut
 
-- 
2.51.0

Attachment: signature.asc
Description: This is a digitally signed message part

Reply via email to