oauth: Add TLS support for oauth_validator tests

The oauth_validator tests don't currently support HTTPS, which makes
testing PGOAUTHCAFILE difficult. Add a localhost certificate to
src/test/ssl and make use of it in oauth_server.py.

In passing, explain the hardcoded use of IPv4 in our issuer identifier,
after intermittent failures on NetBSD led to commit 8d9d5843b. (The new
certificate is still set up for IPv6, to make it easier to improve that
behavior in the future.)

Patch by Jonathan Gonzalez V., with some additional tests and tweaks by
me.

Author: Jonathan Gonzalez V. <jonathan.abdiel@gmail.com>
Discussion: https://postgr.es/m/8a296a2c128aba924bff0ae48af2b88bf8f9188d.camel@gmail.com
This commit is contained in:
Jacob Champion 2026-03-05 10:04:53 -08:00
parent b8d7685835
commit a6483f5ac9
9 changed files with 134 additions and 13 deletions

View file

@ -36,5 +36,6 @@ include $(top_srcdir)/contrib/contrib-global.mk
export PYTHON
export with_libcurl
export with_python
export cert_dir=$(top_srcdir)/src/test/ssl/ssl
endif

View file

@ -80,6 +80,7 @@ tests += {
'PYTHON': python.full_path(),
'with_libcurl': oauth_flow_supported ? 'yes' : 'no',
'with_python': 'yes',
'cert_dir': meson.project_source_root() / 'src/test/ssl/ssl',
},
'deps': [oauth_hook_client],
},

View file

@ -71,9 +71,31 @@ END
$? = $exit_code;
}
# To test against HTTPS with our custom CA, we need to enable PGOAUTHDEBUG and
# PGOAUTHCAFILE. But first, check to make sure the client refuses HTTP and
# untrusted HTTPS connections by default.
my $port = $webserver->port();
my $issuer = "http://127.0.0.1:$port";
unlink($node->data_dir . '/pg_hba.conf');
$node->append_conf(
'pg_hba.conf', qq{
local all test oauth issuer="$issuer" scope="openid postgres"
});
$node->reload;
my $log_start = $node->wait_for_log(qr/reloading configuration files/);
$node->connect_fails(
"user=test dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
"HTTPS is required without debug mode",
expected_stderr =>
qr@OAuth discovery URI "\Q$issuer\E/.well-known/openid-configuration" must use HTTPS@
);
# Switch to HTTPS.
$issuer = "https://127.0.0.1:$port";
unlink($node->data_dir . '/pg_hba.conf');
$node->append_conf(
'pg_hba.conf', qq{
@ -83,7 +105,8 @@ local all testparam oauth issuer="$issuer/param" scope="openid postgres"
});
$node->reload;
my $log_start = $node->wait_for_log(qr/reloading configuration files/);
$log_start =
$node->wait_for_log(qr/reloading configuration files/, $log_start);
# Check pg_hba_file_rules() support.
my $contents = $bgconn->query_safe(
@ -96,16 +119,26 @@ is( $contents,
3|oauth|\{issuer=$issuer/param,"scope=openid postgres",validator=validator\}},
"pg_hba_file_rules recreates OAuth HBA settings");
# To test against HTTP rather than HTTPS, we need to enable PGOAUTHDEBUG. But
# first, check to make sure the client refuses such connections by default.
# Make sure PGOAUTHDEBUG=UNSAFE doesn't disable certificate verification.
$ENV{PGOAUTHDEBUG} = "UNSAFE";
$node->connect_fails(
"user=test dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
"HTTPS is required without debug mode",
"HTTPS trusts only system CA roots by default",
# Note that the latter half of this error message comes from Curl, which has
# had a few variants since 7.61:
#
# - SSL peer certificate or SSH remote key was not OK
# - Peer certificate cannot be authenticated with given CA certificates
# - Issuer check against peer certificate failed
#
# Key off of the "peer certificate" portion, since that seems to have
# remained constant over a long period of time.
expected_stderr =>
qr@OAuth discovery URI "\Q$issuer\E/.well-known/openid-configuration" must use HTTPS@
);
qr/failed to fetch OpenID discovery document:.*peer certificate/i);
$ENV{PGOAUTHDEBUG} = "UNSAFE";
# Now we can use our alternative CA.
$ENV{PGOAUTHCAFILE} = "$ENV{cert_dir}/root+server_ca.crt";
my $user = "test";
$node->connect_ok(

View file

@ -15,7 +15,7 @@ OAuth::Server - runs a mock OAuth authorization server for testing
$server->run;
my $port = $server->port;
my $issuer = "http://127.0.0.1:$port";
my $issuer = "https://127.0.0.1:$port";
# test against $issuer...
@ -27,9 +27,8 @@ This is glue API between the Perl tests and the Python authorization server
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 does not use TLS (it implements a nonstandard, unsafe
issuer at "http://127.0.0.1:<port>"), so libpq in particular will need to set
PGOAUTHDEBUG=UNSAFE to be able to talk to it.
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.
=cut

View file

@ -11,12 +11,17 @@ import functools
import http.server
import json
import os
import ssl
import sys
import time
import urllib.parse
from collections import defaultdict
from typing import Dict
ssl_dir = os.getenv("cert_dir")
ssl_cert = ssl_dir + "/server-localhost-alt-names.crt"
ssl_key = ssl_dir + "/server-localhost-alt-names.key"
class OAuthHandler(http.server.BaseHTTPRequestHandler):
"""
@ -295,7 +300,11 @@ class OAuthHandler(http.server.BaseHTTPRequestHandler):
def config(self) -> JsonObject:
port = self.server.socket.getsockname()[1]
issuer = f"http://127.0.0.1:{port}"
# XXX This IPv4-only Issuer can't be changed to "localhost" unless our
# server also listens on the corresponding IPv6 port when available.
# Otherwise, other processes with ephemeral sockets could accidentally
# interfere with our Curl client, causing intermittent failures.
issuer = f"https://127.0.0.1:{port}"
if self._alt_issuer:
issuer += "/alternate"
elif self._parameterized:
@ -408,9 +417,18 @@ def main():
Starts the authorization server on localhost. The ephemeral port in use will
be printed to stdout.
"""
# XXX Listen exclusively on IPv4. Listening on a dual-stack socket would be
# more true-to-life, but every OS/Python combination in the buildfarm and CI
# would need to provide the functionality first.
s = http.server.HTTPServer(("127.0.0.1", 0), OAuthHandler)
# Speak HTTPS.
# TODO: switch to HTTPSServer with Python 3.14
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_cert_chain(ssl_cert, ssl_key)
s.socket = ssl_context.wrap_socket(s.socket, server_side=True)
# Attach a "cache" dictionary to the server to allow the OAuthHandlers to
# track state across token requests. The use of defaultdict ensures that new
# entries will be created automatically.

View file

@ -0,0 +1,20 @@
# An OpenSSL format CSR config file for creating a server certificate.
#
# This certificate contains SANs for localhost (DNS, IPv4, and IPv6).
[ req ]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[ req_distinguished_name ]
OU = PostgreSQL test suite
# For Subject Alternative Names
[ v3_req ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = localhost
IP.1 = 127.0.0.1
IP.2 = ::1

View file

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDVjCCAj6gAwIBAgIIICYCJxRTBwAwDQYJKoZIhvcNAQELBQAwQjFAMD4GA1UE
Aww3VGVzdCBDQSBmb3IgUG9zdGdyZVNRTCBTU0wgcmVncmVzc2lvbiB0ZXN0IHNl
cnZlciBjZXJ0czAgFw0yNjAyMjcyMjUzMDdaGA8yMDUzMDcxNTIyNTMwN1owIDEe
MBwGA1UECwwVUG9zdGdyZVNRTCB0ZXN0IHN1aXRlMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEA3k/aT/OV8sbJrvhtSgz5eNMCuv7RKdUQw+f52DpZTs85
lTXIRs+l3mXoKRjN1gqzqlHInnJlhxQipqGiJfz4Li8L6jma2yZztFHH+f+YF8Ke
5fCYP1qMxbghqeIRkKgrCEjHUnOhbN5oMi/Ndt9AXWGG/39uk5Xec/Y/J5aZkPVV
blqWYyQQ+4U783lwZs1EUWdfiTVRp8fYADT/2lHjaZaX08vAE5VvCbBv6mPhPfno
F9FIaW+CRuwORisFK8Bd1q/0r5aPZGPi0lokCdaB/cRUHwJK1/HHgyB3N+Lk4swf
z+MfSqj4IaNPW7zn3EV9hgpVwSmB5ES8rzojiGtMDQIDAQABo3AwbjAsBgNVHREE
JTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwHQYDVR0OBBYE
FOZ8KClKVbeYecn8lvAldBXOjQz6MB8GA1UdIwQYMBaAFPKPOmZAUGRIItcugv9W
nsKz7nQKMA0GCSqGSIb3DQEBCwUAA4IBAQDE1FGw20H0Flo3gAGN0ND9G/6wDxWM
MldbXRjqc1E0/+7+Zs6v1jPrNUNEvxy5kHWevUJCIt6y4SYt01JxE4wqEPJ3UBAv
cM0p08mohmN/CHc/lswXx12MZMfaLA1/WRPqvtiGFOrOOPvaRKHO4ORiT1KWmtOO
FgcW9E1Q1iJFK28xdz9NEEBWEurEIr5KGAsCwf9DfQxPJXiS9n98BDI8gPwlse7t
VqyhGVSj+EPbdY2kqkSuPXacdnUGfO6EWo9PFKqhxWMxABLuK0UZzH6/1lMOh1m9
Mm+gtwO5RLBX22V+KIs1uuDTNcveQ2DsZnMZh7lGD05eHYG9hwnC6GNZ
-----END CERTIFICATE-----

View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDeT9pP85Xyxsmu
+G1KDPl40wK6/tEp1RDD5/nYOllOzzmVNchGz6XeZegpGM3WCrOqUciecmWHFCKm
oaIl/PguLwvqOZrbJnO0Ucf5/5gXwp7l8Jg/WozFuCGp4hGQqCsISMdSc6Fs3mgy
L81230BdYYb/f26Tld5z9j8nlpmQ9VVuWpZjJBD7hTvzeXBmzURRZ1+JNVGnx9gA
NP/aUeNplpfTy8ATlW8JsG/qY+E9+egX0Uhpb4JG7A5GKwUrwF3Wr/Svlo9kY+LS
WiQJ1oH9xFQfAkrX8ceDIHc34uTizB/P4x9KqPgho09bvOfcRX2GClXBKYHkRLyv
OiOIa0wNAgMBAAECggEAFchiPkJCV4r12RCbeM2DpjyawGLWcNBhN6jjuLWi6Y9x
d3bRHGsdOAjpMhmtlYLv7sjbrPbNjupAqO4eerVqRfAzLSyeyUlfvfPjcdIC/5UA
x8wGxvJi576ugbxWd0ObD9E9woz07LtwHzbC3ZprbprvRNqiJZDiPp+KuaDOhD7u
6XAM8JilFqfiDN8+xbH2dWdVkdt2OD5wctJbqy6moH9VFVsWsMQr3/vJkSdUPLxa
8ATUubFhO/sqE+KsMZESq5W1Xbj3NwMkvnA92yG9+ED60NPjFzgheZZWSmXe1B/c
XB3G/upvCoHEgKbrnYt05b/ryUbXAZkvi5oL4fp9OwKBgQD4d+Qm4GiKEWvjZ5II
ROfHEyoWOHw9z8ydJIrtOL8ICh5RH8D/v2IaMAacWV5eLoJ7aYC6yIYuWdHQljAi
zltNFrsLFmWXLy91IWfUzIGnFLWeqOmI50vlM8xU54rD/cZ3qtvr2Qk9HHs0dsyB
6cGRf0BPJi04aAEqSZqc8HCXAwKBgQDlDP0MW57bHpqQROQDLIgEX9/rzUNo48Z/
1f27bCkKP+CpizE9eWvGs5rQmUxCNzWULFxIuBbgsubuVP7jO3piY6bRGnvSE6nD
mW0V1mSypVO22Ci/Q8ekkY2+0ZVp3qLPO/cwtI/Ye8kp4xu41I2XgJE8Mo0hEEyJ
N1/1vUJbrwKBgBp3gukVPG2An5JwpOCWnm3ZP8FwMOPQr8YJb3cHdWng0gvoKwHT
HBsYBIxBBMlZgPKucVT0KT7kuHHUnboHazhR9Iig0R+CmjaK4WmMgz8N+K625XF8
2dvHYbulkmWAMdTrcVO1IcPNtd4HzY8FHGZoPKxxr51zjrQ3dO3EuumLAoGATho2
sx8OtPLji2wiP77QhoVWqmYspTh9+Bs00NLZz6fmaImQ+cBMcs3NbXHIYg/HUkYq
FZXIH0iBnCUZYMxoN+J5AHZCYGjaC1tmqfqYDZ54RDHC+y0Wh1QmfDmk9Bu5cmal
LFN1dUEIYCMT0duQiGeLnnYyT2LqZiOesgGd/fsCgYEA2GbKteq+io6HAEt2/yry
xZGaRR8Twg0B8XtD9NHCbgizmZiD/mADgyhkgjUsDIkcMzEt+sA4IK9ORgIYqS+/
q2eY1QRKpoZgJJfE8dU88B35YGqdZuXENR4I7w+JrKCCCk5jSiwylvsBsi1HX8Qu
EdQBBRiwkRnxQ83hqRI3ymw=
-----END PRIVATE KEY-----

View file

@ -31,6 +31,7 @@ SERVERS := server-cn-and-alt-names \
server-ip-in-dnsname \
server-single-alt-name \
server-multiple-alt-names \
server-localhost-alt-names \
server-no-names \
server-revoked
CLIENTS := client client-dn client-revoked client_ext client-long \