From cf12750efd63e53463501f31e296bcc788b86920 Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Wed, 17 Jun 2026 06:31:42 +0000 Subject: [PATCH 1/2] libpq: require encrypted connections for OAUTHBEARER Bearer tokens must be protected in transit (RFC 7628, Sec. 5; RFC 6750, Sec. 5.2), but libpq previously sent them to the server over plaintext TCP connections. Refuse the SASL exchange unless the connection is encrypted with SSL/TLS or GSSAPI, or uses a Unix-domain socket. The previous behavior can be restored for local development and testing with the new unsafe debug option PGOAUTHDEBUG=UNSAFE:plaintext-server. The legacy PGOAUTHDEBUG=UNSAFE format enables it implicitly. --- doc/src/sgml/libpq.sgml | 24 +++++++++- src/interfaces/libpq/fe-auth-oauth.c | 24 ++++++++++ src/interfaces/libpq/oauth-debug.h | 10 +++-- .../modules/oauth_validator/t/001_server.pl | 45 +++++++++++++++++++ 4 files changed, 99 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 7d3c3bb66d8..49634253740 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -10275,6 +10275,14 @@ void PQinitSSL(int do_ssl); client application does not have a usable web browser, for example when running a client via SSH. + + Bearer tokens must be protected in transit + (RFC 7628). + libpq refuses to perform OAuth authentication + unless the connection to the server is encrypted with + SSL/TLS or GSSAPI, + or uses a Unix-domain socket. + The builtin flow will, by default, print a URL to visit and a user code to enter there: @@ -10699,6 +10707,19 @@ PGOAUTHDEBUG=UNSAFE legacy format; enables + + plaintext-server (unsafe) + + + Permits sending the OAuth bearer token to the + PostgreSQL server over an unencrypted + connection. Without this option, OAUTHBEARER authentication requires + a connection encrypted with SSL/TLS + or GSSAPI, or a Unix-domain socket. + + + + call-count (safe) @@ -10724,7 +10745,8 @@ PGOAUTHDEBUG=UNSAFE legacy format; enables Unsafe options (http, trace, - dos-endpoint) require the UNSAFE: prefix. + dos-endpoint, plaintext-server) + require the UNSAFE: prefix. If unsafe options are specified without this prefix, or if an option name is unrecognized, a warning is printed to standard error and that option is ignored. Other valid options in the list continue to work. Safe options diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c index 826f7461cb3..6e24a365adb 100644 --- a/src/interfaces/libpq/fe-auth-oauth.c +++ b/src/interfaces/libpq/fe-auth-oauth.c @@ -1209,6 +1209,30 @@ oauth_exchange(void *opaq, bool final, /* We begin in the initial response phase. */ Assert(inputlen == -1); + /* + * Bearer tokens must be protected in transit (RFC 7628, Sec. 5; + * RFC 6750, Sec. 5.2). Refuse to continue the exchange if the + * connection to the server is neither encrypted (via TLS or GSS) + * nor local, unless the user has explicitly opted into unsafe + * behavior. + */ + { + bool encrypted = conn->ssl_in_use; + +#ifdef ENABLE_GSS + encrypted = encrypted || conn->gssenc; +#endif + + if (!encrypted && + conn->raddr.addr.ss_family != AF_UNIX && + !(oauth_parse_debug_flags() & OAUTHDEBUG_UNSAFE_PLAINTEXT_SERVER)) + { + libpq_append_conn_error(conn, + "OAuth bearer authentication requires an encrypted connection (consider sslmode=require or gssencmode=require)"); + return SASL_FAILED; + } + } + if (!setup_oauth_parameters(conn)) return SASL_FAILED; diff --git a/src/interfaces/libpq/oauth-debug.h b/src/interfaces/libpq/oauth-debug.h index 4f0c87117e1..07f7703a7f3 100644 --- a/src/interfaces/libpq/oauth-debug.h +++ b/src/interfaces/libpq/oauth-debug.h @@ -35,11 +35,13 @@ */ /* allow HTTP (unencrypted) connections */ -#define OAUTHDEBUG_UNSAFE_HTTP (1<<0) +#define OAUTHDEBUG_UNSAFE_HTTP (1<<0) /* log HTTP traffic (exposes secrets) */ -#define OAUTHDEBUG_UNSAFE_TRACE (1<<1) +#define OAUTHDEBUG_UNSAFE_TRACE (1<<1) /* allow zero-second retry intervals */ -#define OAUTHDEBUG_UNSAFE_DOS_ENDPOINT (1<<2) +#define OAUTHDEBUG_UNSAFE_DOS_ENDPOINT (1<<2) +/* allow sending the bearer token to the server over plaintext connections */ +#define OAUTHDEBUG_UNSAFE_PLAINTEXT_SERVER (1<<3) /* mind the gap in values; see OAUTHDEBUG_UNSAFE_MASK below */ @@ -112,6 +114,8 @@ oauth_parse_debug_flags(void) flag = OAUTHDEBUG_UNSAFE_TRACE; else if (strcmp(option, "dos-endpoint") == 0) flag = OAUTHDEBUG_UNSAFE_DOS_ENDPOINT; + else if (strcmp(option, "plaintext-server") == 0) + flag = OAUTHDEBUG_UNSAFE_PLAINTEXT_SERVER; else if (strcmp(option, "call-count") == 0) flag = OAUTHDEBUG_CALL_COUNT; else if (strcmp(option, "plugin-errors") == 0) diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl index 1619fbffd45..ead1d7b4ffb 100644 --- a/src/test/modules/oauth_validator/t/001_server.pl +++ b/src/test/modules/oauth_validator/t/001_server.pl @@ -50,6 +50,8 @@ $node->append_conf('postgresql.conf', "oauth_validator_libraries = 'validator'\n"); # Needed to allow connect_fails to inspect postmaster log: $node->append_conf('postgresql.conf', "log_min_messages = debug2"); +# The plaintext-TCP enforcement tests below need a TCP listener. +$node->append_conf('postgresql.conf', "listen_addresses = '127.0.0.1'\n"); $node->start; $node->safe_psql('postgres', 'CREATE USER test;'); @@ -831,6 +833,49 @@ $node->connect_fails( "fail_validator is used for $user", expected_stderr => qr/FATAL:\s+fail_validator: sentinel error/); +# +# Test that bearer tokens are refused over unencrypted TCP connections +# unless the unsafe plaintext-server debug option is set. +# + +unlink($node->data_dir . '/pg_hba.conf'); +$node->append_conf( + 'pg_hba.conf', qq{ +local all test oauth validator=validator issuer="$issuer" scope="openid postgres" +host all test 127.0.0.1/32 oauth validator=validator issuer="$issuer" scope="openid postgres" +}); +$node->reload; + +$log_start = + $node->wait_for_log(qr/reloading configuration files/, $log_start); + +$user = "test"; +my $tcp_connstr = "host=127.0.0.1 user=$user dbname=postgres" + . " oauth_issuer=$issuer oauth_client_id=f02c6361-0635"; + +# The PGOAUTHDEBUG value set above does not include plaintext-server, so +# this must fail before any flow is run. +$node->connect_fails( + $tcp_connstr, + "bearer tokens are not sent over unencrypted TCP", + expected_stderr => + qr@OAuth bearer authentication requires an encrypted connection@, + log_unlike => [qr/oauth_validator: token=/]); + +{ + local $ENV{PGOAUTHDEBUG} = "$ENV{PGOAUTHDEBUG},plaintext-server"; + + $node->connect_ok( + $tcp_connstr, + "unencrypted TCP is allowed with plaintext-server", + expected_stderr => + qr@Visit https://example\.com/ and enter the code: postgresuser@, + log_like => [ + qr/connection authenticated: identity="test" method=oauth/, + qr/connection authorized/, + ]); +} + # # Test ABI compatibility magic marker # -- 2.43.0