since-v39.diff.txt
text/plain
Filename: since-v39.diff.txt
Type: text/plain
Part: 0
1: 3dc642d68c8 ! 1: 7ee8628abac Add OAUTHBEARER SASL mechanism
@@ Commit message
Grants (RFC 8628). This adds a new auth method, oauth, to pg_hba. When
speaking to a OAuth-enabled server, it looks a bit like this:
- $ psql 'host=example.org oauth_client_id=f02c6361-0635-...'
+ $ psql 'host=example.org oauth_issuer=... oauth_client_id=...'
Visit https://oauth.example.org/login and enter the code: FPQ2-M4BG
The OAuth issuer must support device authorization. No other OAuth flows
- are currently implemented (but clients may provide their own flows; see
- below).
+ are currently implemented (but clients may provide their own flows).
The client implementation requires libcurl and its development headers.
- Pass `curl` to --with-oauth/-Doauth during configuration. The server
+ Pass --with-libcurl/-Dlibcurl=enabled during configuration. The server
implementation does not require additional build-time dependencies, but
- an external validator module must be supplied (see below).
+ an external validator module must be supplied.
Thomas Munro wrote the kqueue() implementation for oauth-curl; thanks!
- = Debug Mode =
-
- A "dangerous debugging mode" may be enabled in libpq, by setting the
- environment variable PGOAUTHDEBUG=UNSAFE. This will do several things
- that you will not want in a production system:
-
- - permits the use of plaintext HTTP in the OAuth provider exchange
- - sprays HTTP traffic, containing several critical secrets, to stderr
- - permits the use of zero-second retry intervals, which can DoS the
- client
-
- = PQauthDataHook =
-
- Clients may override two pieces of OAuth handling using the new
- PQsetAuthDataHook():
-
- - PQAUTHDATA_PROMPT_OAUTH_DEVICE: replaces the default user prompt to
- standard error when using the builtin device authorization flow
-
- - PQAUTHDATA_OAUTH_BEARER_TOKEN: replaces the entire OAuth flow with a
- custom asynchronous implementation
-
- In general, a hook implementation should examine the incoming `type` to
- decide whether or not to handle a specific piece of authdata; if not, it
- should delegate to the previous hook in the chain (retrievable via
- PQgetAuthDataHook()). Otherwise, it should return an integer > 0 and
- follow the authdata-specific instructions. Returning an integer < 0
- signals an error condition and abandons the connection attempt.
-
- == PQAUTHDATA_PROMPT_OAUTH_DEVICE ==
-
- The hook should display the device prompt (URL + code) using whatever
- method it prefers.
-
- == PQAUTHDATA_OAUTH_BEARER_TOKEN ==
-
- The hook should either directly return a Bearer token for the current
- user/issuer/scope combination, if one is available without blocking, or
- else set up an asynchronous callback to retrieve one. See the
- documentation for PQoauthBearerRequest.
-
- = Server-Side Validation =
-
- Because OAuth implementations vary so wildly, and bearer token
- validation is heavily dependent on the issuing party, authn/z is done by
- communicating with an external validator module using callbacks.
- The module is responsible for:
-
- 1. Validate the bearer token. The correct way to do this depends on the
- issuer, but it generally involves either cryptographic operations to
- prove that the token was issued by a trusted party, or the
- presentation of the bearer token to some other party so that _it_ can
- perform validation.
-
- The command MUST maintain confidentiality of the bearer token, since
- in most cases it can be used just like a password. (There are ways to
- cryptographically bind tokens to client certificates, but they are
- way beyond the scope of this commit message.)
-
- If the token cannot be validated, the authorized member of the
- ValidatorModuleResult struct is used to indicate failure.
- Further authentication/authorization is pointless if
- the bearer token wasn't issued by someone you trust.
-
- 3. Authenticate the user, authorize the user, or both:
-
- a. To authenticate the user, use the bearer token to retrieve some
- trusted identifier string for the end user. The exact process for
- this is, again, issuer-dependent. The module wull return the
- authenticated identity in the authn_id member.
-
- b. To optionally authorize the user, in combination with the HBA
- option trust_validator_authz=1 (see below).
-
- The hard part is in determining whether the given token truly
- authorizes the client to use the given role, which must
- unfortunately be left as an exercise to the reader.
-
- This obviously requires some care, as a poorly implemented token
- validator may silently open the entire database to anyone with a
- bearer token. But it may be a more portable approach, since OAuth
- is designed as an authorization framework, not an authentication
- framework. For example, the user's bearer token could carry an
- "allow_superuser_access" claim, which would authorize pseudonymous
- database access as any role. It's then up to the OAuth system
- administrators to ensure that allow_superuser_access is doled out
- only to the proper users.
-
- c. It's possible that the user can be successfully authenticated but
- isn't authorized to connect. In this case, the validator module may
- return the authenticated ID and then fail with false authorized
- member. (This can make it easier to see what's going on in the
- Postgres logs.)
-
- = OAuth HBA Method =
-
- The oauth method supports the following HBA options (but note that two
- of them are not optional, since we have no way of choosing sensible
- defaults):
-
- issuer: Required. The URL of the OAuth issuing party, which the client
- must contact to receive a bearer token.
-
- Some real-world examples as of time of writing:
- - https://accounts.google.com
- - https://login.microsoft.com/[tenant-id]/v2.0
-
- scope: Required. The OAuth scope(s) required for the server to
- authenticate and/or authorize the user. This is heavily
- deployment-specific, but a simple example is "openid email".
-
- map: Optional. Specify a standard PostgreSQL user map; this works
- the same as with other auth methods such as peer. If a map is
- not specified, the user ID returned by the token validator
- must exactly match the role that's being requested (but see
- trust_validator_authz, below).
-
- trust_validator_authz:
- Optional. When set to 1, this allows the token validator to
- take full control of the authorization process. Standard user
- mapping is skipped: if the validator command succeeds, the
- client is allowed to connect under its desired role and no
- further checks are done.
-
Several TODOs:
- - don't retry forever if the server won't accept our token
- perform several sanity checks on the OAuth issuer's responses
- handle cases where the client has been set up with an issuer and
scope, but the Postgres server wants to use something different
@@ Commit message
- fix libcurl initialization thread-safety
- harden the libcurl flow implementation
- figure out pgsocket/int difference on Windows
- - fix intermittent failure in the cleanup callback tests (race
- condition?)
- - support require_auth
- fill in documentation stubs
- support protocol "variants" implemented by major providers
- implement more helpful handling of HBA misconfigurations
- use logdetail during auth failures
- - allow passing the configured issuer to the oauth_validator_command, to
- deal with multi-issuer setups
- - fill in documentation stubs
- ...and more.
Co-authored-by: Daniel Gustafsson <daniel@yesql.se>
@@ src/backend/libpq/auth-oauth.c (new)
+ * The "credentials" construction is what we receive in our auth value.
+ *
+ * Since that spec is subordinate to HTTP (i.e. the HTTP Authorization
-+ * header format; RFC 7235 Sec. 2), the "Bearer" scheme string must be
-+ * compared case-insensitively. (This is not mentioned in RFC 6750, but
-+ * it's pointed out in RFC 7628 Sec. 4.)
++ * header format; RFC 9110 Sec. 11), the "Bearer" scheme string must be
++ * compared case-insensitively. (This is not mentioned in RFC 6750, but the
++ * OAUTHBEARER spec points it out: RFC 7628 Sec. 4.)
+ *
+ * Invalid formats are technically a protocol violation, but we shouldn't
+ * reflect any information about the sensitive Bearer token back to the
+ * client; log at COMMERROR instead.
-+ *
-+ * TODO: handle the Authorization spec, RFC 7235 Sec. 2.1.
+ */
+static const char *
+validate_token_format(const char *header)
@@ src/backend/libpq/auth-oauth.c (new)
+ return false;
+ }
+
++ /*
++ * Log any authentication results even if the token isn't authorized; it
++ * might be useful for auditing or troubleshooting.
++ */
++ if (ret->authn_id)
++ set_authn_id(port, ret->authn_id);
++
+ if (!ret->authorized)
+ {
++ ereport(LOG,
++ errmsg("OAuth bearer authentication failed for user \"%s\"",
++ port->user_name),
++ errdetail_log("Validator failed to authorize the provided token."));
++
+ status = false;
+ goto cleanup;
+ }
+
-+ if (ret->authn_id)
-+ set_authn_id(port, ret->authn_id);
-+
+ if (port->hba->oauth_skip_usermap)
+ {
+ /*
@@ src/interfaces/libpq/Makefile: backend_src = $(top_srcdir)/src/backend
endif
## src/interfaces/libpq/exports.txt ##
-@@ src/interfaces/libpq/exports.txt: PQcancelFinish 202
- PQsocketPoll 203
+@@ src/interfaces/libpq/exports.txt: PQsocketPoll 203
PQsetChunkedRowsMode 204
PQgetCurrentTimeUSec 205
-+PQsetAuthDataHook 206
-+PQgetAuthDataHook 207
-+PQdefaultAuthDataHook 208
+ PQservice 206
++PQsetAuthDataHook 207
++PQgetAuthDataHook 208
++PQdefaultAuthDataHook 209
## src/interfaces/libpq/fe-auth-oauth-curl.c (new) ##
@@
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+
+ ++field;
+ }
++
++ /*
++ * We don't allow duplicate field names; error out if the target has
++ * already been set.
++ */
++ if (ctx->active)
++ {
++ field = ctx->active;
++
++ if ((field->type == JSON_TOKEN_ARRAY_START && *field->target.array)
++ || (field->type != JSON_TOKEN_ARRAY_START && *field->target.scalar))
++ {
++ oauth_parse_set_error(ctx, "field \"%s\" is duplicated",
++ field->name);
++ return JSON_SEM_ACTION_FAILED;
++ }
++ }
+ }
+
+ return JSON_SUCCESS;
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ return JSON_SEM_ACTION_FAILED;
+ }
+
-+ /*
-+ * We don't allow duplicate field names; error out if the target has
-+ * already been set.
-+ */
-+ if ((field->type == JSON_TOKEN_ARRAY_START && *field->target.array)
-+ || (field->type != JSON_TOKEN_ARRAY_START && *field->target.scalar))
-+ {
-+ oauth_parse_set_error(ctx, "field \"%s\" is duplicated",
-+ field->name);
-+ return JSON_SEM_ACTION_FAILED;
-+ }
-+
+ if (field->type != JSON_TOKEN_ARRAY_START)
+ {
+ Assert(ctx->nested == 1);
++ Assert(!*field->target.scalar);
+
+ *field->target.scalar = strdup(token);
+ if (!*field->target.scalar)
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+
+ /*
+ * Multi-threaded applications must set CURLOPT_NOSIGNAL. This requires us
-+ * to handle the possibility of SIGPIPE ourselves.
-+ *
-+ * TODO: handle SIGPIPE via pq_block_sigpipe(), or via a
-+ * CURLOPT_SOCKOPTFUNCTION maybe...
++ * to handle the possibility of SIGPIPE ourselves using pq_block_sigpipe;
++ * see pg_fe_run_oauth_flow().
+ */
+ CHECK_SETOPT(actx, CURLOPT_NOSIGNAL, 1L, return false);
+ if (!curl_info->ares_num)
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+
+
+/*
-+ * The top-level, nonblocking entry point for the libcurl implementation. This
-+ * will be called several times to pump the async engine.
++ * The core nonblocking libcurl implementation. This will be called several
++ * times to pump the async engine.
+ *
+ * The architecture is based on PQconnectPoll(). The first half drives the
+ * connection state forward as necessary, returning if we're not ready to
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ * OAUTH_STEP_TOKEN_REQUEST and OAUTH_STEP_WAIT_INTERVAL to regularly ping the
+ * provider.
+ */
-+PostgresPollingStatusType
-+pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock)
++static PostgresPollingStatusType
++pg_fe_run_oauth_flow_impl(PGconn *conn, pgsocket *altsock)
+{
+ fe_oauth_state *state = conn->sasl_state;
+ struct async_ctx *actx;
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ appendPQExpBufferStr(&conn->errorMessage, "\n");
+
+ return PGRES_POLLING_FAILED;
++}
++
++/*
++ * The top-level entry point. This is a convenient place to put necessary
++ * wrapper logic before handing off to the true implementation, above.
++ */
++PostgresPollingStatusType
++pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock)
++{
++ PostgresPollingStatusType result;
++#ifndef WIN32
++ sigset_t osigset;
++ bool sigpipe_pending;
++ bool masked;
++
++ /*---
++ * Ignore SIGPIPE on this thread during all Curl processing.
++ *
++ * Because we support multiple threads, we have to set up libcurl with
++ * CURLOPT_NOSIGNAL, which disables its default global handling of
++ * SIGPIPE. From the Curl docs:
++ *
++ * libcurl makes an effort to never cause such SIGPIPE signals to
++ * trigger, but some operating systems have no way to avoid them and
++ * even on those that have there are some corner cases when they may
++ * still happen, contrary to our desire.
++ *
++ * Note that libcurl is also at the mercy of its DNS resolution and SSL
++ * libraries; if any of them forget a MSG_NOSIGNAL then we're in trouble.
++ * Modern platforms and libraries seem to get it right, so this is a
++ * difficult corner case to exercise in practice, and unfortunately it's
++ * not really clear whether it's necessary in all cases.
++ */
++ masked = (pq_block_sigpipe(&osigset, &sigpipe_pending) == 0);
++#endif
++
++ result = pg_fe_run_oauth_flow_impl(conn, altsock);
++
++#ifndef WIN32
++ if (masked)
++ {
++ /*
++ * Undo the SIGPIPE mask. Assume we may have gotten EPIPE (we have no
++ * way of knowing at this level).
++ */
++ pq_reset_sigpipe(&osigset, sigpipe_pending, true /* EPIPE, maybe */ );
++ }
++#endif
++
++ return result;
+}
## src/interfaces/libpq/fe-auth-oauth.c (new) ##
@@ src/interfaces/libpq/libpq-fe.h: extern int PQenv2encoding(void);
+ */
+ PostgresPollingStatusType (*async) (PGconn *conn,
+ struct _PGoauthBearerRequest *request,
-+ SOCKTYPE *altsock);
++ SOCKTYPE * altsock);
+
+ /*
+ * Callback to clean up custom allocations. A hook implementation may use
@@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
+#include <stdio.h>
+#include <stdlib.h>
+
++#ifdef WIN32
++#include <winsock2.h>
++#else
++#include <sys/socket.h>
++#endif
++
+#include "getopt_long.h"
+#include "libpq-fe.h"
+
+static int handle_auth_data(PGauthData type, PGconn *conn, void *data);
++static PostgresPollingStatusType async_cb(PGconn *conn,
++ PGoauthBearerRequest *req,
++ pgsocket *altsock);
+
+static void
+usage(char *argv[])
@@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
+ fprintf(stderr, " --expected-scope SCOPE fail if received scopes do not match SCOPE\n");
+ fprintf(stderr, " --expected-uri URI fail if received configuration link does not match URI\n");
+ fprintf(stderr, " --no-hook don't install OAuth hooks (connection will fail)\n");
++ fprintf(stderr, " --hang-forever don't ever return a token (combine with connect_timeout)\n");
+ fprintf(stderr, " --token TOKEN use the provided TOKEN value\n");
+}
+
++/* --options */
+static bool no_hook = false;
++static bool hang_forever = false;
+static const char *expected_uri = NULL;
+static const char *expected_scope = NULL;
+static char *token = NULL;
@@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
+ {"expected-uri", required_argument, NULL, 1001},
+ {"no-hook", no_argument, NULL, 1002},
+ {"token", required_argument, NULL, 1003},
++ {"hang-forever", no_argument, NULL, 1004},
+ {0}
+ };
+
@@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
+ token = optarg;
+ break;
+
++ case 1004: /* --hang-forever */
++ hang_forever = true;
++ break;
++
+ default:
+ usage(argv);
+ return 1;
@@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
+ if (no_hook || (type != PQAUTHDATA_OAUTH_BEARER_TOKEN))
+ return 0;
+
++ if (hang_forever)
++ {
++ /* Start asynchronous processing. */
++ req->async = async_cb;
++ return 1;
++ }
++
+ if (expected_uri)
+ {
+ if (!req->openid_configuration)
@@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
+
+ req->token = token;
+ return 1;
++}
++
++static PostgresPollingStatusType
++async_cb(PGconn *conn, PGoauthBearerRequest *req, pgsocket *altsock)
++{
++ if (hang_forever)
++ {
++ /*
++ * This code tests that nothing is interfering with libpq's handling
++ * of connect_timeout.
++ */
++ static pgsocket sock = PGINVALID_SOCKET;
++
++ if (sock == PGINVALID_SOCKET)
++ {
++ /* First call. Create an unbound socket to wait on. */
++#ifdef WIN32
++ WSADATA wsaData;
++ int err;
++
++ err = WSAStartup(MAKEWORD(2, 2), &wsaData);
++ if (err)
++ {
++ perror("WSAStartup failed");
++ return PGRES_POLLING_FAILED;
++ }
++#endif
++ sock = socket(AF_INET, SOCK_DGRAM, 0);
++ if (sock == PGINVALID_SOCKET)
++ {
++ perror("failed to create datagram socket");
++ return PGRES_POLLING_FAILED;
++ }
++ }
++
++ /* Make libpq wait on the (unreadable) socket. */
++ *altsock = sock;
++ return PGRES_POLLING_READING;
++ }
++
++ req->token = token;
++ return PGRES_POLLING_OK;
+}
## src/test/modules/oauth_validator/t/001_server.pl (new) ##
@@ src/test/modules/oauth_validator/t/001_server.pl (new)
+});
+$node->reload;
+
-+my ($log_start, $log_end);
-+$log_start = $node->wait_for_log(qr/reloading configuration files/);
++my $log_start = $node->wait_for_log(qr/reloading configuration files/);
+
+
+# To test against HTTP rather than HTTPS, we need to enable PGOAUTHDEBUG. But
@@ src/test/modules/oauth_validator/t/001_server.pl (new)
+$ENV{PGOAUTHDEBUG} = "UNSAFE";
+
+my $user = "test";
-+if ($node->connect_ok(
-+ "user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
-+ "connect",
-+ expected_stderr =>
-+ qr@Visit https://example\.com/ and enter the code: postgresuser@))
-+{
-+ $log_end = $node->wait_for_log(qr/connection authorized/, $log_start);
-+ $node->log_check(
-+ "user $user: validator receives correct parameters",
-+ $log_start,
-+ log_like => [
-+ qr/oauth_validator: token="9243959234", role="$user"/,
-+ qr/oauth_validator: issuer="\Q$issuer\E", scope="openid postgres"/,
-+ ]);
-+ $node->log_check(
-+ "user $user: validator sets authenticated identity",
-+ $log_start,
-+ log_like =>
-+ [ qr/connection authenticated: identity="test" method=oauth/, ]);
-+ $log_start = $log_end;
-+}
++$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";
-+if ($node->connect_ok(
-+ "user=$user dbname=postgres oauth_issuer=$issuer/alternate oauth_client_id=f02c6361-0636",
-+ "connect",
-+ expected_stderr =>
-+ qr@Visit https://example\.org/ and enter the code: postgresuser@))
-+{
-+ $log_end = $node->wait_for_log(qr/connection authorized/, $log_start);
-+ $node->log_check(
-+ "user $user: validator receives correct parameters",
-+ $log_start,
-+ log_like => [
-+ qr/oauth_validator: token="9243959234-alt", role="$user"/,
-+ qr|oauth_validator: issuer="\Q$issuer/alternate\E", scope="openid postgres alt"|,
-+ ]);
-+ $node->log_check(
-+ "user $user: validator sets authenticated identity",
-+ $log_start,
-+ log_like =>
-+ [ qr/connection authenticated: identity="testalt" method=oauth/, ]);
-+ $log_start = $log_end;
-+}
++$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(
@@ src/test/modules/oauth_validator/t/001_server.pl (new)
+$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);
+
-+if ($node->connect_fails(
-+ "$common_connstr user=test",
-+ "validator must set authn_id",
-+ expected_stderr => qr/OAuth bearer authentication failed/))
-+{
-+ $log_end =
-+ $node->wait_for_log(qr/FATAL:\s+OAuth bearer authentication failed/,
-+ $log_start);
-+
-+ $node->log_check(
-+ "validator must set authn_id: breadcrumbs are logged",
-+ $log_start,
-+ log_like => [
-+ qr/connection authenticated: identity=""/,
-+ qr/DETAIL:\s+Validator provided no identity/,
-+ qr/FATAL:\s+OAuth bearer authentication failed/,
-+ ]);
-+
-+ $log_start = $log_end;
-+}
++$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);
++
++$node->connect_fails(
++ "$common_connstr user=test",
++ "validator must authorize token explicitly",
++ expected_stderr => qr/OAuth bearer authentication failed/,
++ log_like => [
++ qr/connection authenticated: identity="test\@example\.org"/,
++ qr/DETAIL:\s+Validator failed to authorize the provided token/,
++ qr/FATAL:\s+OAuth bearer authentication failed/,
++ ]);
+
+#
+# Test user mapping.
@@ src/test/modules/oauth_validator/t/001_server.pl (new)
+
+# 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");
+
+$node->reload;
+$log_start =
@@ src/test/modules/oauth_validator/t/001_server.pl (new)
+
+# The test user should work as before.
+$user = "test";
-+if ($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_start = $node->wait_for_log(qr/connection authorized/, $log_start);
-+}
++$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";
@@ src/test/modules/oauth_validator/t/002_client.pl (new)
+ );
+}
+
++# 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/);
++
+done_testing();
## src/test/modules/oauth_validator/t/OAuth/Server.pm (new) ##
@@ src/test/modules/oauth_validator/t/oauth_server.py (new)
+ "response_types_supported": ["token"],
+ "subject_types_supported": ["public"],
+ "id_token_signing_alg_values_supported": ["RS256"],
-+ "grant_types_supported": ["urn:ietf:params:oauth:grant-type:device_code"],
++ "grant_types_supported": [
++ "authorization_code",
++ "urn:ietf:params:oauth:grant-type:device_code",
++ ],
+ }
+
+ @property
@@ src/test/modules/oauth_validator/validator.c (new)
+ .validate_cb = validate_token
+};
+
++/* GUCs */
+static char *authn_id = NULL;
++static bool authorize_tokens = true;
+
+/*---
+ * Extension entry point. Sets up GUCs for use by tests:
@@ src/test/modules/oauth_validator/validator.c (new)
+ * - oauth_validator.authn_id Sets the user identifier to return during token
+ * validation. Defaults to the username in the
+ * startup packet.
++ *
++ * - oauth_validator.authorize_tokens
++ * Sets whether to successfully validate incoming
++ * tokens. Defaults to true.
+ */
+void
+_PG_init(void)
@@ src/test/modules/oauth_validator/validator.c (new)
+ PGC_SIGHUP,
+ 0,
+ NULL, NULL, NULL);
++ DefineCustomBoolVariable("oauth_validator.authorize_tokens",
++ "Should tokens be marked valid?",
++ NULL,
++ &authorize_tokens,
++ true,
++ PGC_SIGHUP,
++ 0,
++ NULL, NULL, NULL);
+
+ MarkGUCPrefixReserved("oauth_validator");
+}
@@ src/test/modules/oauth_validator/validator.c (new)
+}
+
+/*
-+ * Validator implementation. Logs the incoming data and authorizes the token;
-+ * the behavior can be modified via the module's GUC settings.
++ * Validator implementation. Logs the incoming data and authorizes the token by
++ * default; the behavior can be modified via the module's GUC settings.
+ */
+static ValidatorModuleResult *
+validate_token(ValidatorModuleState *state, const char *token, const char *role)
@@ src/test/modules/oauth_validator/validator.c (new)
+ MyProcPort->hba->oauth_issuer,
+ MyProcPort->hba->oauth_scope);
+
-+ res->authorized = true;
++ res->authorized = authorize_tokens;
+ if (authn_id)
+ res->authn_id = pstrdup(authn_id);
+ else
-: ----------- > 2: de155343c81 squash! Add OAUTHBEARER SASL mechanism
2: 566d90d30a7 ! 3: 661de01c4ed DO NOT MERGE: Add pytest suite for OAuth
@@ src/test/python/client/test_oauth.py (new)
+ "subject_types_supported": ["public"],
+ "id_token_signing_alg_values_supported": ["RS256"],
+ "grant_types_supported": [
-+ "urn:ietf:params:oauth:grant-type:device_code"
++ "authorization_code",
++ "urn:ietf:params:oauth:grant-type:device_code",
+ ],
+ }
+
@@ src/test/python/client/test_oauth.py (new)
+ # that break the HTTP protocol. Just return and have the server
+ # close the socket.
+ return
++ except ssl.SSLError as err:
++ # FIXME OpenSSL 3.4 introduced an incompatibility with Python's
++ # TLS error handling, resulting in a bogus "[SYS] unknown error"
++ # on some platforms. Hopefully this is fixed in 2025's set of
++ # maintenance releases and this case can be removed.
++ #
++ # https://github.com/python/cpython/issues/127257
++ #
++ if "[SYS] unknown error" in str(err):
++ return
++ raise
+
+ super().shutdown_request(request)
+