1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
|
#
# Exercises the API for custom OAuth client flows, using the oauth_hook_client
# test driver.
#
# 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;
if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\boauth\b/)
{
plan skip_all =>
'Potentially unsafe test oauth not enabled in PG_TEST_EXTRA';
}
#
# Cluster Setup
#
my $node = PostgreSQL::Test::Cluster->new('primary');
$node->init;
$node->append_conf('postgresql.conf', "log_connections = on\n");
$node->append_conf('postgresql.conf',
"oauth_validator_libraries = 'validator'\n");
$node->start;
$node->safe_psql('postgres', 'CREATE USER test;');
# These tests don't use the builtin flow, and we don't have an authorization
# server running, so the address used here shouldn't matter. Use an invalid IP
# address, so if there's some cascade of errors that causes the client to
# attempt a connection, we'll fail noisily.
my $issuer = "https://256.256.256.256";
my $scope = "openid postgres";
unlink($node->data_dir . '/pg_hba.conf');
$node->append_conf(
'pg_hba.conf', qq{
local all test oauth issuer="$issuer" scope="$scope"
});
$node->reload;
my $log_start = $node->wait_for_log(qr/reloading configuration files/);
$ENV{PGOAUTHDEBUG} = "UNSAFE";
#
# Tests
#
my $user = "test";
my $base_connstr = $node->connstr() . " user=$user";
my $common_connstr =
"$base_connstr oauth_issuer=$issuer oauth_client_id=myID";
sub test
{
my ($test_name, %params) = @_;
my $flags = [];
if (defined($params{flags}))
{
$flags = $params{flags};
}
my @cmd = ("oauth_hook_client", @{$flags}, $common_connstr);
note "running '" . join("' '", @cmd) . "'";
my ($stdout, $stderr) = run_command(\@cmd);
if (defined($params{expected_stdout}))
{
like($stdout, $params{expected_stdout}, "$test_name: stdout matches");
}
if (defined($params{expected_stderr}))
{
like($stderr, $params{expected_stderr}, "$test_name: stderr matches");
}
else
{
is($stderr, "", "$test_name: no stderr");
}
}
test(
"basic synchronous hook can provide a token",
flags => [
"--token", "my-token",
"--expected-uri", "$issuer/.well-known/openid-configuration",
"--expected-scope", $scope,
],
expected_stdout => qr/connection succeeded/);
$node->log_check("validator receives correct token",
$log_start,
log_like => [ qr/oauth_validator: token="my-token", role="$user"/, ]);
if ($ENV{with_libcurl} ne 'yes')
{
# libpq should help users out if no OAuth support is built in.
test(
"fails without custom hook installed",
flags => ["--no-hook"],
expected_stderr =>
qr/no custom OAuth flows are available, and libpq was not built with libcurl support/
);
}
# connect_timeout should work if the flow doesn't respond.
$common_connstr = "$common_connstr connect_timeout=1";
test(
"connect_timeout interrupts hung client flow",
flags => ["--hang-forever"],
expected_stderr => qr/failed: timeout expired/);
# Remove the timeout for later tests.
$common_connstr = "$base_connstr oauth_issuer=$issuer oauth_client_id=myID";
# Test various misbehaviors of the client hook.
my @cases = (
{
flag => "--misbehave=no-hook",
expected_error =>
qr/user-defined OAuth flow provided neither a token nor an async callback/,
},
{
flag => "--misbehave=fail-async",
expected_error => qr/user-defined OAuth flow failed/,
},
{
flag => "--misbehave=no-token",
expected_error => qr/user-defined OAuth flow did not provide a token/,
},
{
flag => "--misbehave=no-socket",
expected_error =>
qr/user-defined OAuth flow did not provide a socket for polling/,
});
foreach my $c (@cases)
{
test(
"hook misbehavior: $c->{'flag'}",
flags => [ $c->{'flag'} ],
expected_stderr => $c->{'expected_error'});
}
done_testing();
|