# # Tests the libpq builtin OAuth flow, as well as server-side HBA and validator # setup. # # Copyright (c) 2021-2025, PostgreSQL Global Development Group #
use strict;
use warnings FATAL => 'all';
use JSON::PP qw(encode_json);
use MIME::Base64 qw(encode_base64);
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
use FindBin;
use lib $FindBin::RealBin;
use OAuth::Server;
if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\boauth\b/)
{
plan skip_all => 'Potentially unsafe test oauth not enabled in PG_TEST_EXTRA';
}
unless (check_pg_config("#define HAVE_SYS_EVENT_H 1")
or check_pg_config("#define HAVE_SYS_EPOLL_H 1"))
{
plan skip_all => 'OAuth server-side tests are not supported on this platform';
}
if ($ENV{with_libcurl} ne 'yes')
{
plan skip_all => 'client-side OAuth not supported by this build';
}
if ($ENV{with_python} ne 'yes')
{
plan skip_all => 'OAuth tests require --with-python to run';
}
my $node = PostgreSQL::Test::Cluster->new('primary');
$node->init;
$node->append_conf('postgresql.conf', "log_connections = all\n");
$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");
$node->start;
$node->safe_psql('postgres', 'CREATE USER test;');
$node->safe_psql('postgres', 'CREATE USER testalt;');
$node->safe_psql('postgres', 'CREATE USER testparam;');
# Save a background connection for later configuration changes.
my $bgconn = $node->background_psql('postgres');
my $webserver = OAuth::Server->new();
$webserver->run();
END
{
my $exit_code = $?;
$webserver->stop() if defined $webserver; # might have been SKIP'd
unlink($node->data_dir . '/pg_hba.conf');
$node->append_conf( 'pg_hba.conf', qq{
local all test oauth issuer="$issuer" scope="openid postgres"
local all testalt oauth issuer="$issuer/.well-known/oauth-authorization-server/alternate" scope="openid postgres alt"
local all testparam oauth issuer="$issuer/param" scope="openid postgres"
});
$node->reload;
my $log_start = $node->wait_for_log(qr/reloading configuration files/);
# Check pg_hba_file_rules() support.
my $contents = $bgconn->query_safe(
qq(SELECT rule_number, auth_method, options
FROM pg_hba_file_rules
ORDER BY rule_number;));
is( $contents,
qq{1|oauth|\{issuer=$issuer,"scope=openid postgres",validator=validator\} 2|oauth|\{issuer=$issuer/.well-known/oauth-authorization-server/alternate,"scope=openid postgres alt",validator=validator\} 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.
$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@
);
$ENV{PGOAUTHDEBUG} = "UNSAFE";
my $user = "test";
$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/,
]);
# The /alternate issuer uses slightly different parameters, along with an # OAuth-style discovery document.
$user = "testalt";
$node->connect_ok( "user=$user dbname=postgres oauth_issuer=$issuer/alternate oauth_client_id=f02c6361-0636", "connect as testalt",
expected_stderr =>
qr@Visit https://example\.org/ and enter the code: postgresuser@,
log_like => [
qr/oauth_validator: token="9243959234-alt", role="$user"/,
qr|oauth_validator: issuer="\Q$issuer/.well-known/oauth-authorization-server/alternate\E", scope="openid postgres alt"|,
qr/connection authenticated: identity="testalt" method=oauth/,
qr/connection authorized/,
]);
# 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\)@
);
$user = "test"; foreach my $c (@cases)
{
my $connstr = "user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635 require_auth=$c->{'require_auth'}";
if (defined $c->{'failure'})
{
$node->connect_fails(
$connstr, "require_auth=$c->{'require_auth'} fails",
expected_stderr => $c->{'failure'});
} else
{
$node->connect_ok(
$connstr, "require_auth=$c->{'require_auth'} succeeds",
expected_stderr =>
qr@Visit https://example\.com/ and enter the code: postgresuser@
);
}
}
# Make sure the client_id and secret are correctly encoded. $vschars contains # every allowed character for a client_id/_secret (the "VSCHAR" class). # $vschars_esc is additionally backslash-escaped for inclusion in a # single-quoted connection string.
my $vschars = " !\"#\$%&'()*+,-./0123456789:;<=>?\@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
my $vschars_esc = " !\"#\$%&\\'()*+,-./0123456789:;<=>?\@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
$node->connect_ok( "user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id='$vschars_esc'", "escapable characters: client_id",
expected_stderr =>
qr@Visit https://example\.com/ and enter the code: postgresuser@);
$node->connect_ok( "user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id='$vschars_esc' oauth_client_secret='$vschars_esc'", "escapable characters: client_id and secret",
expected_stderr =>
qr@Visit https://example\.com/ and enter the code: postgresuser@);
# # Further tests rely on support for specific behaviors in oauth_server.py. To # trigger these behaviors, we ask for the special issuer .../param (which is set # up in HBA for the testparam user) and encode magic instructions into the # oauth_client_id. #
my $common_connstr = "user=testparam dbname=postgres oauth_issuer=$issuer/param ";
my $base_connstr = $common_connstr;
sub connstr
{
my (%params) = @_;
my $json = encode_json(\%params);
my $encoded = encode_base64($json, "");
# Make sure the param system works end-to-end first.
$node->connect_ok(
connstr(), "connect to /param",
expected_stderr =>
qr@Visit https://example\.com/ and enter the code: postgresuser@);
$node->connect_ok(
connstr(stage => 'token', retries => 1), "token retry",
expected_stderr =>
qr@Visit https://example\.com/ and enter the code: postgresuser@);
$node->connect_ok(
connstr(stage => 'token', retries => 2), "token retry (twice)",
expected_stderr =>
qr@Visit https://example\.com/ and enter the code: postgresuser@);
$node->connect_ok(
connstr(stage => 'all', retries => 1, interval => 2), "token retry (two second interval)",
expected_stderr =>
qr@Visit https://example\.com/ and enter the code: postgresuser@);
$node->connect_ok(
connstr(stage => 'all', retries => 1, interval => JSON::PP::null), "token retry (default interval)",
expected_stderr =>
qr@Visit https://example\.com/ and enter the code: postgresuser@);
$node->connect_ok(
connstr(stage => 'all', content_type => 'application/json;charset=utf-8'), "content type with charset",
expected_stderr =>
qr@Visit https://example\.com/ and enter the code: postgresuser@);
$node->connect_ok(
connstr(
stage => 'all',
content_type => "application/json \t;\t charset=utf-8"), "content type with charset (whitespace)",
expected_stderr =>
qr@Visit https://example\.com/ and enter the code: postgresuser@);
$node->connect_ok(
connstr(stage => 'device', uri_spelling => "verification_url"), "alternative spelling of verification_uri",
expected_stderr =>
qr@Visit https://example\.com/ and enter the code: postgresuser@);
$node->connect_fails(
connstr(stage => 'device', huge_response => JSON::PP::true), "bad device authz response: overlarge JSON",
expected_stderr =>
qr/failed to obtain device authorization: response is too large/);
$node->connect_fails(
connstr(stage => 'token', huge_response => JSON::PP::true), "bad token response: overlarge JSON",
expected_stderr =>
qr/failed to obtain access token: response is too large/);
my $nesting_limit = 16;
$node->connect_ok(
connstr(
stage => 'device',
nested_array => $nesting_limit,
nested_object => $nesting_limit), "nested arrays and objects, up to parse limit",
expected_stderr =>
qr@Visit https://example\.com/ and enter the code: postgresuser@);
$node->connect_fails(
connstr(stage => 'device', nested_array => $nesting_limit + 1), "bad discovery response: overly nested JSON array",
expected_stderr =>
qr/failed to parse device authorization: JSON is too deeply nested/);
$node->connect_fails(
connstr(stage => 'device', nested_object => $nesting_limit + 1), "bad discovery response: overly nested JSON object",
expected_stderr =>
qr/failed to parse device authorization: JSON is too deeply nested/);
# Count the number of calls to the internal flow when multiple retries are # triggered. The exact number depends on many things -- the TCP stack, the # version of Curl in use, random chance -- but a ridiculously high number # suggests something is wrong with our ability to clear multiplexer events after # they're no longer applicable.
my ($ret, $stdout, $stderr) = $node->psql( 'postgres', "SELECT 'connected for call count'",
extra_params => ['-w'],
connstr => connstr(stage => 'token', retries => 2),
on_error_stop => 0);
is($ret, 0, "call count connection succeeds");
like(
$stderr,
qr@Visit https://example\.com/ and enter the code: postgresuser@, "call count: stderr matches");
my $count_pattern = qr/\[libpq\] total number of polls: (\d+)/; if (like($stderr, $count_pattern, "call count: count is printed"))
{ # For reference, a typical flow with two retries might take between 5-15 # calls to the client implementation. And while this will probably continue # to change across OSes and Curl updates, we're likely in trouble if we see # hundreds or thousands of calls.
$stderr =~ $count_pattern;
cmp_ok($1, '<', 100, "call count is reasonably small");
}
# Stress test: make sure our builtin flow operates correctly even if the client # application isn't respecting PGRES_POLLING_READING/WRITING signals returned # from PQconnectPoll().
$base_connstr = "$common_connstr port=" . $node->port . " host=" . $node->host;
my @cmd = ( "oauth_hook_client", "--no-hook", "--stress-async",
connstr(stage => 'all', retries => 1, interval => 1));
# # This section of tests reconfigures the validator module itself, rather than # the OAuth server. #
# Searching the logs is easier if OAuth parameter discovery isn't cluttering # things up; hardcode the discovery URI. (Scope is hardcoded to empty to cover # that case as well.)
$common_connstr = "dbname=postgres oauth_issuer=$issuer/.well-known/openid-configuration oauth_scope='' oauth_client_id=f02c6361-0635";
# Misbehaving validators must fail shut.
$bgconn->query_safe("ALTER SYSTEM SET oauth_validator.authn_id TO ''");
$node->reload;
$log_start =
$node->wait_for_log(qr/reloading configuration files/, $log_start);
$node->connect_fails( "$common_connstr user=test", "validator must set authn_id",
expected_stderr => qr/OAuth bearer authentication failed/,
log_like => [
qr/connection authenticated: identity=""/,
qr/DETAIL:\s+Validator provided no identity/,
qr/FATAL:\s+OAuth bearer authentication failed/,
]);
# Even if a validator authenticates the user, if the token isn't considered # valid, the connection fails.
$bgconn->query_safe( "ALTER SYSTEM SET oauth_validator.authn_id TO 'test\@example.org'");
$bgconn->query_safe( "ALTER SYSTEM SET oauth_validator.authorize_tokens TO false");
$node->reload;
$log_start =
$node->wait_for_log(qr/reloading configuration files/, $log_start);
# Allow "user@example.com" to log in under the test role.
unlink($node->data_dir . '/pg_ident.conf');
$node->append_conf( 'pg_ident.conf', qq{
oauthmap user\@example.com test
});
# test and testalt use the map; testparam uses ident delegation.
unlink($node->data_dir . '/pg_hba.conf');
$node->append_conf( 'pg_hba.conf', qq{
local all test oauth issuer="$issuer" scope="" map=oauthmap
local all testalt oauth issuer="$issuer" scope="" map=oauthmap
local all testparam oauth issuer="$issuer" scope="" delegate_ident_mapping=1
});
# To start, have the validator use the role names as authn IDs.
$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.authn_id");
$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.authorize_tokens");
# The test and testalt roles should no longer map correctly.
$node->connect_fails( "$common_connstr user=test", "mismatched username map (test)",
expected_stderr => qr/OAuth bearer authentication failed/);
$node->connect_fails( "$common_connstr user=testalt", "mismatched username map (testalt)",
expected_stderr => qr/OAuth bearer authentication failed/);
# Have the validator identify the end user as user@example.com.
$bgconn->query_safe( "ALTER SYSTEM SET oauth_validator.authn_id TO 'user\@example.com'");
$node->reload;
$log_start =
$node->wait_for_log(qr/reloading configuration files/, $log_start);
# Now the test role can be logged into. (testalt still can't be mapped.)
$node->connect_ok( "$common_connstr user=test", "matched username map (test)",
expected_stderr =>
qr@Visit https://example\.com/ and enter the code: postgresuser@);
$node->connect_fails( "$common_connstr user=testalt", "mismatched username map (testalt)",
expected_stderr => qr/OAuth bearer authentication failed/);
# testparam ignores the map entirely.
$node->connect_ok( "$common_connstr user=testparam", "delegated ident (testparam)",
expected_stderr =>
qr@Visit https://example\.com/ and enter the code: postgresuser@);
# With multiple validators, every HBA line must explicitly declare one.
my $result = $node->restart(fail_ok => 1);
is($result, 0, 'restart fails without explicit validators in oauth HBA entries');
$log_start = $node->wait_for_log(
qr/authentication method "oauth" requires argument "validator" to be set/,
$log_start);
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"
local all testalt oauth validator=fail_validator issuer="$issuer/.well-known/oauth-authorization-server/alternate" scope="openid postgres alt"
});
$node->restart;
$log_start = $node->wait_for_log(qr/ready to accept connections/, $log_start);
# The test user should work as before.
$user = "test";
$node->connect_ok( "user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635", "validator is used for $user",
expected_stderr =>
qr@Visit https://example\.com/ and enter the code: postgresuser@,
log_like => [qr/connection authorized/]);
# testalt should be routed through the fail_validator.
$user = "testalt";
$node->connect_fails( "user=$user dbname=postgres oauth_issuer=$issuer/.well-known/oauth-authorization-server/alternate oauth_client_id=f02c6361-0636", "fail_validator is used for $user",
expected_stderr => qr/FATAL:\s+fail_validator: sentinel error/);
# # Test ABI compatibility magic marker #
$node->append_conf('postgresql.conf', "oauth_validator_libraries = 'magic_validator'\n");
unlink($node->data_dir . '/pg_hba.conf');
$node->append_conf( 'pg_hba.conf', qq{
local all test oauth validator=magic_validator issuer="$issuer" scope="openid postgres"
});
$node->restart;
$log_start = $node->wait_for_log(qr/ready to accept connections/, $log_start);
$node->connect_fails( "user=test dbname=postgres oauth_issuer=$issuer/.well-known/oauth-authorization-server/alternate oauth_client_id=f02c6361-0636", "magic_validator is used for $user",
expected_stderr =>
qr/FATAL:\s+OAuth validator module "magic_validator": magic number mismatch/
);
$node->stop;
done_testing();
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.15 Sekunden
(vorverarbeitet am 2026-08-10)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.